REST API Design: The Constraints and HTTP Semantics Everyone Skips (Part 1 of 3)

⏱ 10 min read

REST is everywhere. Almost every web application, mobile app, and microservice you interact with today exposes or consumes a REST API. Yet most developers learn REST by cargo-culting patterns they see in tutorials. They slap HTTP verbs on endpoints and call it done.

This is the first post in a series on REST API design that goes deeper than the tutorials. We'll start with the part everyone skips: what REST actually is, the constraints Roy Fielding defined it by, and the HTTP semantics that most APIs get subtly wrong. That means methods and status codes.


What REST Actually Is 🔗

REST stands for Representational State Transfer. Roy Fielding defined it in his 2000 doctoral dissertation, where he described the architectural style that underpins the web itself.

The key insight in Fielding's thesis is that REST is not a protocol or a standard. It is an architectural style: a set of constraints that, applied together, produce desirable properties in a distributed hypermedia system. If your API doesn't satisfy those constraints, it isn't REST. It may still be a perfectly useful HTTP API, but calling it REST is technically incorrect.

This distinction matters more than it might seem. Fielding designed REST to be highly scalable, loosely coupled, and evolvable over time. Those properties emerge from the constraints, not from any single choice like "use JSON" or "put the ID in the URL". Understanding the constraints is understanding the why behind every REST best practice.


The Six Architectural Constraints 🔗

Fielding defined six constraints. The first five are mandatory; the sixth is optional.

1. Client-Server 🔗

A uniform interface separates the client and server. The client is responsible for the user interface and user experience. The server is responsible for data storage, business logic, and security.

Why it matters: This separation improves portability of the UI across platforms and scalability of the server components. Neither side needs to know the implementation details of the other. A web client, a mobile client, and a CLI tool can all consume the same API. The server doesn't care which one is making the request.

2. Stateless 🔗

Each request from a client to a server must contain all the information necessary to process it. The client keeps all session state. The server stores no client context between requests.

Why it matters: Statelessness enables horizontal scaling. Any server in a pool can handle any request because no server holds session state. It also improves visibility: you can understand a single request in isolation, which helps with debugging and monitoring. And it improves reliability, since partial failures don't leave dangling server-side sessions.

The practical consequence: if a user is authenticated, the authentication token travels with every single request. The server never says "I remember you from last time." It re-validates the token on every call.

3. Cacheable 🔗

Responses must define themselves as cacheable or non-cacheable. If a response is cacheable, the client (or an intermediate proxy) can reuse that response for equivalent future requests.

Why it matters: Caching eliminates some client-server interactions entirely, improving efficiency, scalability, and perceived performance. HTTP already has a rich caching model (Cache-Control, ETag, Last-Modified, Expires), and REST makes full use of it.

Getting caching right requires deliberate thought about:

  • Which resources change frequently vs. infrequently
  • Whether two requests are truly equivalent (same query parameters, same Accept header, etc.)
  • Conditional requests (If-None-Match, If-Modified-Since) for cheap revalidation

4. Uniform Interface 🔗

This is the central constraint that distinguishes REST from other network-based styles. It has four sub-constraints:

Identification of resources. Stable identifiers (URIs) identify each resource. The resource and its representation are distinct. The server might store user data in a relational database. It can represent that data as JSON, XML, or HTML, depending on what the client requests.

Manipulation of resources through representations. When a client holds a representation of a resource, including any metadata, it has enough information to modify or delete the resource. It just needs permission.

Self-descriptive messages. Each message includes enough information to describe how to process it. A Content-Type: application/json header tells the recipient exactly how to interpret the body. This decouples message processing from the application logic.

Hypermedia as the engine of application state (HATEOAS). Clients interact with the application entirely through hypermedia that the server provides dynamically. I cover this one in detail in the last post of this series.

Why it matters: A uniform interface simplifies the architecture and makes it easier to evolve parts of the system independently. The cost is efficiency: the interface generalizes rather than optimizing for any specific use case. That is an explicit and intentional trade-off in REST.

5. Layered System 🔗

The architecture must be composable from hierarchical layers. A client cannot tell whether it's talking directly to the server or to an intermediary. You can insert intermediaries, such as load balancers, API gateways, caches, and security proxies, transparently.

Why it matters: Layers let you enforce security policies at the boundary, cache aggressively at edge nodes, and load-balance without clients knowing. They also let you evolve infrastructure independently from application logic. HTTPS termination at a load balancer is a layered system in action.

6. Code on Demand (Optional) 🔗

Servers can extend client functionality by transferring executable code, like JavaScript running in a browser. This constraint is optional because it reduces visibility. You can't easily audit what code a client will execute, and it creates coupling.

Most API design work ignores code on demand. It shows up primarily in browser-based applications, not in service-to-service APIs.


HTTP Methods 🔗

HTTP provides a small vocabulary of methods (also called verbs). REST maps operations on resources to this vocabulary. Each method carries a defined semantic and a set of properties.

The Core Methods 🔗

Method Semantics Safe Idempotent Request Body Response Body
GET Retrieve a resource or collection Yes Yes No Yes
POST Create a subordinate resource, or trigger a process No No Yes Yes
PUT Replace a resource entirely No Yes Yes Yes
PATCH Partially update a resource No No (usually) Yes Yes
DELETE Remove a resource No Yes Optional Optional
HEAD Same as GET but body omitted Yes Yes No No
OPTIONS Retrieve supported methods and CORS preflight Yes Yes No Yes

Safe means the method should not have any observable side effects on the server. Clients (and proxies) can prefetch or retry safe methods freely.

Idempotent means that multiple identical requests produce the same server state as a single request. A DELETE /users/42 should have the same effect whether called once or ten times. This property is what makes retries safe.

Common Misuses 🔗

POST for everything. Using POST for reads, updates, and deletes is a common anti-pattern. It throws away idempotency and prevents HTTP-layer caching.

PUT when you mean PATCH. PUT replaces the entire resource. If a client sends a PUT with a partial payload and the server accepts it, the result behaves like PATCH. On paper, though, it's still PUT. This creates subtle bugs when clients omit fields they didn't intend to clear.

DELETE with a body. While technically possible, HTTP clients and intermediaries poorly support bodies on DELETE requests. If you need to delete multiple resources, prefer a separate endpoint or use PATCH on a collection.

POST vs. PUT for creation. Use POST when the server assigns the identifier: you POST to /orders and the server returns /orders/789. Use PUT when the client specifies the identifier: you PUT to /users/alice to create or replace that resource.


HTTP Status Codes 🔗

Status codes are not decoration. They carry semantic meaning and drive client behavior. Using the wrong status code is like raising the wrong exception type. Technically it works, but everyone downstream makes the wrong decisions because of it.

The Five Classes 🔗

1xx, Informational: Rarely used in REST APIs. 100 Continue tells the client it can proceed with a large request body.

2xx, Success: The request was received, understood, and accepted.

3xx, Redirection: Further action needs to be taken to complete the request.

4xx, Client Error: The client sent an invalid request. Retrying the same request unchanged will not succeed.

5xx, Server Error: The server failed to process a valid request. The client may retry.

Key Codes in Detail 🔗

200 OK: General success. The response body contains the result. Appropriate for GET, PUT, PATCH.

201 Created: A new resource was created. The Location header should point to the new resource URI. Appropriate for POST and PUT when creating.

202 Accepted: The server accepted the request for processing, but processing isn't complete. Used for async operations. The response should explain how the client can check status.

204 No Content: Success, but there is nothing to return. Common for DELETE and some PUTs.

301 Moved Permanently: The resource has a new permanent URI. Clients should update their bookmarks.

304 Not Modified: The client's cached copy is still fresh. No body is returned. Used with conditional requests.

400 Bad Request: The server cannot process the request because of a client error (malformed JSON, invalid field values, missing required parameters). Return a body that explains what is wrong.

401 Unauthorized: The request lacks valid authentication credentials. Despite the name, this code means "unauthenticated": the client needs to provide credentials.

403 Forbidden: The client is authenticated but does not have permission to access the resource. Don't return 404 as a security measure unless you want to hide the existence of the resource.

404 Not Found: The resource does not exist. Also used intentionally when a 403 would reveal information you want to protect.

405 Method Not Allowed: The HTTP method is not supported for this resource. The response must include an Allow header listing valid methods.

409 Conflict: The request conflicts with the current state of the resource. Common for optimistic concurrency failures or duplicate creation attempts.

410 Gone: The resource existed but has been permanently deleted. Distinct from 404; useful for caches and clients to know not to ask again.

422 Unprocessable Entity: The request is syntactically valid but semantically wrong (valid JSON, but the business rules reject it). Preferred over 400 for domain validation failures.

429 Too Many Requests: Rate limit exceeded. Include a Retry-After header.

500 Internal Server Error: Generic server failure. Never expose stack traces or internal details.

503 Service Unavailable: The server is temporarily unable to handle requests (overloaded, maintenance). Include Retry-After if known.

The Mistake of Returning 200 for Everything 🔗

A common anti-pattern is returning 200 OK with a body that contains an error flag:

HTTP/1.1 200 OK

{ "success": false, "error": "User not found" }

This forces every client to parse the body to know whether the request succeeded. It breaks HTTP-aware intermediaries (caches will cache failure responses), APM tools, and alerting systems. Use the status code as it was designed to be used.


Closing 🔗

Get the constraints and the HTTP semantics right, and you've built the foundation. Neither one is optional trivia. The rest of REST design only works if two things hold: a client that trusts your status codes, and an architecture that respects statelessness.

PS: Let me know if I missed anything, ping me on Twitter/X or LinkedIn, and follow along there if you'd like more of this. Let's chat.

Get the next one by email

Every post here goes out by email too - one a week.
.NET, messaging, and distributed systems, with the trade-offs the docs leave out.