RESTful API Design: Resource Contracts, HTTP Semantics & Status Code Discipline
REST is not a set of conventions teams pick up informally — it is a uniform interface constraint that, when followed precisely, produces APIs that are self-describing, cache-correct, and safe to retry. This article derives RESTful design from first principles: resource modeling, HTTP method idempotency contracts, status code semantics, pagination invariants, and RFC 7807 structured error bodies.
API Architecture & System Resilience
RESTful API Design: Resource Contracts, HTTP Semantics & Status Code Discipline
Senior engineers don't just wire services together — they design the boundary: the contract, the trust model, the failure envelope, and the signal pipeline that proves it's working. In REST APIs, that boundary starts with the URL and the HTTP method. The most expensive mistake in backend development is building an API that works — that returns the right data and accepts the right payloads — but that violates HTTP semantics in ways that clients cannot detect until they retry a failed request and discover they've created duplicate orders, applied a discount twice, or deleted the wrong resource. This article derives RESTful design from first principles so that every decision carries an engineering rationale, not a convention.
Series positioning: This is Part 1 of the API Architecture & System Resilience series. It establishes the REST design vocabulary that all subsequent articles build on — Part 2 (API Gateways) assumes fluency in method semantics and status code contracts.
1. The Uniform Interface Constraint
REST is not "JSON over HTTP." It is an architectural style defined by Roy Fielding in his 2000 dissertation, and its most important constraint is the uniform interface: every resource is identified by a URI, and every interaction uses the same small set of standardized HTTP methods with well-defined semantics. When teams violate this constraint, they create proprietary RPC protocols with HTTP as the transport — and lose every benefit that standard tooling, caches, proxies, and clients expect.
1.1 Resource Modeling: Nouns, Not Verbs
The URI identifies a resource (a noun), not an operation (a verb). The HTTP method is the verb. This distinction matters because HTTP caches, load balancers, and API gateways make decisions based on the method — a GET is safe to cache and retry; a POST is not. Verb-in-URL APIs lose all of this infrastructure-level intelligence.
Resource hierarchy depth should not exceed 3 levels: /{collection}/{id}/{sub-collection}. Deeper hierarchies (e.g., /users/{id}/orders/{orderId}/items/{itemId}/discounts) create tight coupling between URL structure and database schema, make API evolution harder, and produce URLs that are impossible to remember. If you need a 4th level, extract it as a top-level resource.
1.2 Collection vs Singleton Resources
| Pattern | URI Example | Semantics |
|---|---|---|
| Collection | /orders |
A set of resources; supports GET (list) and POST (create) |
| Singleton | /orders/42 |
A single resource; supports GET, PUT, PATCH, DELETE |
| Sub-collection | /orders/42/items |
Items belonging to order 42; independent collection |
| Singleton sub-resource | /orders/42/status |
A scalar property modeled as a resource when mutation matters |

2. HTTP Method Semantics & Idempotency Contracts
2.1 The Five Methods and Their Contracts
The critical distinction is between safe (no side effects — GET, HEAD, OPTIONS) and idempotent (multiple identical requests produce the same server state — GET, PUT, DELETE). POST and non-deterministic PATCH are neither.
2.2 The PUT vs PATCH Idempotency Trap
If your PATCH endpoint accepts relative operations (adjustQuantity: -5), it is no longer idempotent and cannot be safely retried without an idempotency key. Make this explicit in your API contract — clients who retry on network failure will corrupt your data.
2.3 POST for Non-Idempotent Creation
The Location header is mandatory on 201 Created — it tells the client where the new resource lives without requiring a second request. Omitting it forces clients to parse the response body to determine the new resource URI, which is not standardized.
3. Status Code Taxonomy
3.1 2xx — Success
| Code | Name | When to Use |
|---|---|---|
200 OK |
Success with body | GET, PUT, PATCH responses with a body |
201 Created |
Resource created | POST that creates a new resource; include Location header |
202 Accepted |
Async processing | Request accepted but processing is async (job queued); include job status URL |
204 No Content |
Success, no body | DELETE that succeeds; PATCH/PUT when response body is not needed |
Return 202 Accepted with a Location header pointing to a job status endpoint for long-running operations. Never make clients wait synchronously for operations that take more than 5 seconds. The job status endpoint should return 200 with {"status": "pending"|"running"|"completed"|"failed"}.
3.2 4xx — Client Errors (Never Retry Without Code Change)
400 and 422 are NOT synonyms. Returning 400 for semantic validation errors (endDate before startDate) forces clients to parse the error message body to understand the root cause. Use 422 for field-level semantic violations and reserve 400 for structural/syntactic malformation. GraphQL APIs that always return 200 make this worse — distinguish the cases.
3.3 5xx — Server Errors (Potentially Retryable)
| Code | Retryable? | Client Action |
|---|---|---|
500 Internal Server Error |
Sometimes | Retry with backoff only if idempotent operation |
502 Bad Gateway |
Yes | Upstream service error; retry with backoff |
503 Service Unavailable |
Yes | Service overloaded or deploying; respect Retry-After |
504 Gateway Timeout |
Yes (carefully) | Upstream timed out; retry only if idempotent |
4. RFC 7807 Problem Details — Structured Error Bodies
The type field is a URI that uniquely identifies the error class. Clients can branch on it with a simple string equality check — no regex, no message parsing. The URI does not need to resolve to a live page (though it should), but it must be stable and unique per error type.
Think of type as an enum value that is globally unique because it is a URI. Two different APIs can both define a "not-found" error with different type URIs, and a client can distinguish them. This is the machine-readable contract that error codes provide in gRPC — REST needed RFC 7807 to get there.
5. Pagination Patterns
5.1 Why Offset Pagination Fails at Scale
5.2 Cursor-Based Pagination
The cursor is a base64-encoded opaque token encoding the last-seen position (typically the id and createdAt of the last item). Clients treat it as opaque — they must not decode or construct cursors. Insertions before the cursor position do not affect subsequent pages.
5.3 Keyset Pagination — Database-Native Scale

6. API Versioning Strategies
6.1 The Three Approaches
| Strategy | Example | Pros | Cons |
|---|---|---|---|
| URL path | /v1/orders |
Explicit, cacheable, simple to test | URLs change; old versions must be maintained |
| Header | Accept: application/vnd.example.v2+json |
Clean URLs; content negotiation standard | Invisible in browser, harder to test |
| Query param | /orders?version=2 |
Simple to add | Non-standard; pollutes cache keys |
URL path versioning (/v1/) is the pragmatic choice for most APIs: it is explicit, cache-correct, and testable with any HTTP client. Reserve header versioning for APIs where URL stability is more important than version clarity (e.g., public APIs with published bookmarks).
6.2 When Breaking Changes Are Unavoidable
7. Filtering, Sorting & Field Selection
7.1 Query Parameter Conventions
7.2 Contract-First Design with OpenAPI 3.1
Enable validateResponses: true in staging and CI environments. Most teams validate only requests. Response validation catches schema drift — when the database returns a null that your schema marks as required, or when a field type changes silently — before it reaches clients in production.
Summary
| Concept | Rule |
|---|---|
| Resource URIs | Nouns only. Hierarchy max 3 levels. Collections are plural. |
| GET / HEAD | Safe + idempotent. Never modify state. Cache-eligible. |
| POST | Creates resources. Not idempotent. Always return Location header on 201. |
| PUT | Full replacement. Idempotent. All fields required; omitted = deleted/defaulted. |
| PATCH | Partial update. Idempotent only if deterministic (set, not increment). |
| DELETE | Idempotent. Returns 204 on success, 404 on repeat — both are correct. |
| Status codes | 400 = malformed; 422 = semantically invalid; 409 = conflict; never conflate. |
| Error bodies | RFC 7807 application/problem+json. type is a stable URI. Clients branch on type. |
| Pagination | Keyset for scale; cursor for stability; offset only for UI-driven small datasets. |
| Versioning | URL path (/v1/) for most APIs. Deprecate fields before removal. |
| Contract-first | OpenAPI 3.1 schema validated in CI for both requests and responses. |
What's Next
In Part 2, we examine the API Gateway — the single enforcement point that sits in front of all your well-designed REST resources and applies JWT validation, rate limiting, circuit breaking, and trace ID injection as a unified infrastructure layer. Part 2: API Gateways →
This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.