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

⏱ 11 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 - slapping HTTP verbs on endpoints and calling 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 - methods and status codes - that most APIs get subtly wrong.


What REST Actually Is 🔗

REST stands for Representational State Transfer. It was defined by Roy Fielding 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, when 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. REST is designed to be highly scalable, loosely coupled, and evolvable over time. Those properties emerge from the constraints, not from any individual 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 🔗

The client and server are separated by a uniform interface. 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 without the server caring which one is making the request.

2. Stateless 🔗

Each request from a client to a server must contain all the information necessary to understand and process the request. Session state is kept entirely on the client. 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 - a single request can be understood in isolation for debugging and monitoring - and 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) is permitted to 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 is built to make 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 - Resources are identified using stable identifiers (URIs). The resource and its representation are distinct. The server might store user data in a relational database, but 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, provided it has 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. Message processing is decoupled from the application.

Hypermedia as the engine of application state (HATEOAS) - Clients interact with the application entirely through hypermedia provided dynamically by the server. 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 is generalized, not optimized 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 is connected directly to the server or to an intermediary. Intermediaries - load balancers, API gateways, caches, security proxies - can be inserted transparently.

Why it matters: Layers allow you to enforce security policies at the boundary, cache aggressively at edge nodes, load-balance without clients knowing, and 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 - JavaScript in a browser being the canonical example. This constraint is optional because it reduces visibility (you can't easily audit what code a client will execute) and 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 that 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, you have implicit PATCH semantics with PUT semantics on paper. This creates subtle bugs when clients omit fields they didn't intend to clear.

DELETE with a body - While technically possible, bodies on DELETE requests are poorly supported by HTTP clients and intermediaries. 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 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 request was accepted for processing but processing is not 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 genuinely 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 - a client that trusts your status codes and an architecture that respects statelessness are what make the rest of REST design work at all.

Next in this series: how to design the resources themselves, version them without breaking clients, and choose an authentication pattern that matches your actual threat model.

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.

Enjoyed this?

I write about .NET, messaging, and distributed systems most weeks - the parts that don't make it into a LinkedIn post.