from resource design to safe versioning
Anyone who designs REST APIs without clear patterns builds in inconsistencies that multiply over months. Shaping resource structure, status codes, error formats, pagination and versioning according to proven patterns separates APIs that survive in production from ones that produce breaking changes with every release.
Table of Contents
- 1. What REST patterns actually solve
- 2. Resource design: nouns, hierarchies and sub-resources
- 3. HTTP status codes: precise instead of convenient
- 4. Error formats: RFC 9457 and consistent error bodies
- 5. Pagination: cursor, offset and link headers
- 6. Versioning: URL path, headers and content negotiation
- 7. OpenAPI specification: schema first instead of code first
- 8. Idempotency, safe methods and retry logic
- 9. REST patterns in direct comparison
- 10. Summary
- 11. FAQ
1. What REST patterns actually solve
A REST pattern is not an academic convention but a proven solution structure for a recurring API design problem. The difference between a consistent API and a chaotic one rarely lies in the technology used, but in the decisions made during design: How are resources named? What does a 200 response actually mean? How does the API tell the client that its request was invalid? These decisions, once made consistently and documented, reduce the cognitive load for developers who consume or maintain the API.
In practice, you see APIs where errors come back as 200 OK with a {"error": true} body, resources are named plural in one place and singular in another, and pagination works via query parameters on some endpoints and via a custom header on others. Such inconsistencies arise because every feature was implemented as an isolated decision without shared REST patterns. The following sections cover the fifty most important patterns, from resource design through error formats to OpenAPI specifications and idempotency.
2. Resource design: nouns, hierarchies and sub-resources
The most fundamental REST pattern is the consistent use of nouns instead of verbs in URL paths. A URL represents a resource, not an action. /orders is correct, /getOrders is not. HTTP methods take over the semantics of the action: GET /orders reads, POST /orders creates, PUT /orders/{id} replaces completely, PATCH /orders/{id} updates partially, DELETE /orders/{id} deletes. This REST pattern makes the API self-explanatory and enables HTTP caching, because GET requests are marked as safe and idempotent.
Hierarchical resources are represented through nested URL paths, but only up to a sensible depth. /customers/{customerId}/orders/{orderId} is understandable and expresses the relationship. /customers/{id}/orders/{orderId}/items/{itemId}/metadata/{key} is too deep, here you should flatten the sub-resources and use references in the response body instead. The REST pattern for actions on resources that cannot be expressed as an HTTP method (for example, cancelling an order) is a dedicated sub-path: POST /orders/{id}/cancellation, the noun stands for the action, without verbs in the path.
# REST Resource Design Patterns, curl examples
# List resource (Collection)
GET /api/v1/orders?status=pending&limit=20&cursor=eyJpZCI6MTIzfQ==
# Single resource
GET /api/v1/orders/4711
# Sub-resource (order items)
GET /api/v1/orders/4711/items
# Action as sub-resource (not a verb in path)
POST /api/v1/orders/4711/cancellation
Content-Type: application/json
{ "reason": "customer_request", "notify": true }
# Partial update with PATCH
PATCH /api/v1/orders/4711
Content-Type: application/merge-patch+json
{ "shippingAddress": { "city": "Berlin" } }
# Idempotent creation with PUT + client-generated ID
PUT /api/v1/carts/session-abc-123
Content-Type: application/json
{ "items": [] }
Plural resource names are the clear convention: /products instead of /product, /users instead of /user. Consistency matters more than the grammatical elegance of individual names. Kebab-case for multi-word path segments: /shipping-addresses instead of /shippingAddresses. Query parameters use camelCase: ?sortField=createdAt. These conventions may seem petty, but in large teams without written API guidelines, exactly this is where inconsistencies arise that developers later have to paper over with adapter code.
3. HTTP status codes: precise instead of convenient
The most common mistake in API design is overusing 200 OK and 500 Internal Server Error. The correct REST pattern: status codes communicate semantics, not just success or failure. 201 Created signals that a new resource was created and the URL of the new resource is in the Location header. 204 No Content signals a successful operation with no return value, typical for DELETE. 202 Accepted indicates that a request was accepted but processing happens asynchronously, with a link to the status resource in the response.
In the error space, precision matters even more. 400 Bad Request means syntactically or semantically invalid input. 401 Unauthorized means missing or invalid authentication. 403 Forbidden means authenticated but not authorized. 404 Not Found means the resource does not exist. 409 Conflict means a state conflict (for example, a duplicate key). 422 Unprocessable Content is the correct code for validation errors, the request is syntactically correct but cannot be processed on a content level. 429 Too Many Requests for rate limiting, always with a Retry-After header. This precision allows clients to implement differentiated error handling instead of treating every error the same.
4. Error formats: RFC 9457 and consistent error bodies
RFC 9457 (Problem Details for HTTP APIs, successor to RFC 7807) defines a standardized JSON error format supported by all modern API frameworks. The REST pattern: every error path returns the same structure type. The required fields are type (a URI reference describing the error type), title (a human-readable summary of the error) and status (the HTTP status code). Optional but recommended: detail (a concrete error description for this request), instance (a URI identifying the specific error case) and domain-specific extension fields such as violations for validation errors.
The Content-Type header for problem detail responses is application/problem+json. This lets clients automatically distinguish between normal and error responses. The REST pattern for validation errors: an overarching problem detail object with status: 422 and an array violations that describes each invalid field with its path and the error message. This pattern can be parsed directly by API clients and saves custom error format documentation, because RFC 9457 is known and self-explanatory.
// RFC 9457 Problem Detail, 422 Validation Error
// Content-Type: application/problem+json
{
"type": "https://mironsoft.de/errors/validation-failed",
"title": "Validation failed",
"status": 422,
"detail": "3 fields contain invalid values.",
"instance": "/api/v1/orders/requests/req-7f3a9c",
"violations": [
{
"field": "shippingAddress.postalCode",
"message": "Postal code must be 5 digits",
"rejectedValue": "123"
},
{
"field": "items[0].quantity",
"message": "Quantity must be at least 1",
"rejectedValue": 0
},
{
"field": "paymentMethod",
"message": "Unknown payment method",
"rejectedValue": "bitcoin_unsupported"
}
]
}
5. Pagination: cursor, offset and link headers
Offset-based pagination (?page=3&pageSize=20) is intuitive but has a fundamental problem: if a record is deleted or inserted between two API calls, the pages shift. The correct REST pattern for production-grade APIs with frequently changing data volumes is cursor-based pagination. The cursor encodes a stable reference point, typically the ID or the timestamp of the last element on the current page. ?cursor=eyJpZCI6NDcxMX0=&limit=20 delivers the next page regardless of changes that occurred in the meantime.
The REST pattern for communicating navigation links: the HTTP Link header per RFC 5988 with rel="next", rel="prev" and rel="first". Alternatively, or in addition, a pagination object in the response body. The advantage of the link header: clients do not need to construct the URL themselves but can simply follow the next link. This decouples clients from the pagination implementation, a breaking change to the cursor format only changes the header value, not the client logic. The response body should never calculate a maximum page count if the data volume is not known; instead, the absence of a next link indicates the end of the collection.
6. Versioning: URL path, headers and content negotiation
API versioning is unavoidable once an API is consumed by external clients. The best-known REST pattern is URL path versioning: /api/v1/orders, /api/v2/orders. The advantage: explicit, easily cacheable, clearly identifiable in logs and monitoring. The disadvantage: URLs are actually resource identifiers, not version markers, /api/v1/orders/4711 and /api/v2/orders/4711 address the same order. For strict REST, header versioning is cleaner: Accept: application/vnd.mironsoft.v2+json or a custom header API-Version: 2.
In practice, URL versioning dominates, because it is visible in browser dev tools, cURL and documentation without configuration. The decisive REST pattern is not the format of versioning but the commit discipline behind it: minor changes (new optional fields, new endpoints) are backward compatible and do not bump the version. Breaking changes (field renames, changed status codes, removed fields) require a new major version. Both versions run in parallel for a defined deprecation period while clients migrate. This period and the EOL date must be communicated in the Sunset header and in the API documentation.
# API versioning patterns
# URL-Path versioning (most common)
GET /api/v1/products/123
GET /api/v2/products/123
# Header versioning (strict REST)
GET /api/products/123
Accept: application/vnd.mironsoft.v2+json
# Sunset header for deprecation notice
HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 01 Aug 2026 00:00:00 GMT
Link: <https://mironsoft.de/api/v2/products/123>; rel="successor-version"
# Content negotiation for response format
GET /api/v1/orders/4711
Accept: application/json # default JSON
Accept: application/ld+json # JSON-LD
Accept: application/vnd.api+json # JSON:API format
# Conditional requests, ETag and If-None-Match
GET /api/v1/products/123
HTTP/1.1 200 OK
ETag: "a3f4c8b92d"
Cache-Control: max-age=60, private
GET /api/v1/products/123
If-None-Match: "a3f4c8b92d"
HTTP/1.1 304 Not Modified
7. OpenAPI specification: schema first instead of code first
The most important process shift in modern API design is the move from code-first to schema-first: the OpenAPI specification is created first, and server stubs, client SDKs and documentation are generated from it. The REST pattern for team workflows: the openapi.yaml is the contract between backend and frontend. Changes to the API are first submitted as an OpenAPI pull request, reviewed and merged, before implementation begins. This prevents API implementations that drift from frontend expectations and enables parallel development through a mock server.
The OpenAPI specification should define reusable schemas in components/schemas and reference them via $ref. The REST pattern for consistent schemas: all timestamps as ISO 8601 strings (format: date-time), all IDs as strings (even if internally a UUID or integer), all monetary amounts as integers in the smallest currency unit (cents, not euros). nullable: false as the default, fields that can be null are marked explicitly. additionalProperties: false in request schemas to reject unknown fields. These conventions in the schema make the generated SDK self-explanatory and reduce integration errors.
8. Idempotency, safe methods and retry logic
Idempotency means that repeated identical requests produce the same state as a single request. This is not the same as identical responses: DELETE /orders/4711 returns 204 No Content on the first call and 404 Not Found on the second, yet it is still idempotent because the resulting state is the same afterward. The REST pattern for retry safety: safe methods (GET, HEAD, OPTIONS) are always idempotent. PUT and DELETE are idempotent by HTTP specification. POST is not, here you need an idempotency-key pattern.
The REST pattern for idempotent POST requests is a client-generated Idempotency-Key header (a UUID). The server stores the key and the response for a defined period (typically 24 hours). If the same request arrives again with the same key, the server returns the cached response without executing the operation again. This is indispensable for payment APIs, order creation and every operation where a network interruption leaves the client uncertain about the outcome. Without this pattern, retries lead to duplicate orders, duplicate charges or database inconsistencies.
9. REST patterns in direct comparison
Many everyday API design decisions can be made in different ways, with substantial differences in consistency, cacheability and client compatibility. Choosing the right REST pattern is not a matter of style, it has a direct impact on the maintainability of the API across versions.
| Design decision | Antipattern | Recommended REST pattern | Benefit |
|---|---|---|---|
| Reporting errors | 200 {"error":true} |
422 + RFC 9457 Body |
Clients can branch on status code |
| Executing an action | POST /cancelOrder |
POST /orders/{id}/cancellation |
Verbs in the path avoided |
| Pagination | ?page=3&size=20 |
?cursor=base64&limit=20 |
Stable under insertions/deletions |
| POST idempotency | No safeguard | Idempotency-Key: UUID |
Retries without side effects |
| Timestamps | Unix timestamp integer | ISO 8601 string | Self-explanatory, timezone-safe |
Antipatterns rarely arise from ignorance, but from time pressure and missing team conventions. A living style guide that exists directly in the API specification as an x-mironsoft-guidelines extension and is checked by a linter ensures that new endpoints follow the same REST patterns, automatically, without manual code reviews for every design question.
Mironsoft
API design, OpenAPI specification and REST architecture
REST APIs that stay consistent in production?
We analyze existing APIs, identify inconsistencies and build OpenAPI specifications that serve as a contract between backend, frontend and QA, with complete error formats, pagination and a versioning strategy.
API review
Analysis of existing APIs for inconsistencies, antipatterns and missing error formats
OpenAPI creation
Schema-first OpenAPI 3.1 specifications with reusable schemas and validation
Versioning strategy
Deprecation processes, sunset headers and breaking-change strategies for stable APIs
10. Summary
The most important REST and OpenAPI patterns for production-grade APIs always solve the same underlying problem: APIs without clear conventions accumulate inconsistencies that complicate integrations and produce breaking changes. Nouns instead of verbs in URLs make the API self-explanatory. Precise HTTP status codes enable differentiated client error handling. RFC 9457 error formats standardize error bodies without custom documentation. Cursor pagination survives data changes reliably. Idempotency keys make POST requests retry-safe.
The biggest lever lies in the schema-first approach: an OpenAPI specification as a team contract from which code is generated prevents divergence between backend implementation and frontend expectations from the outset. Combined with an API linter that catches REST pattern violations in the CI pipeline, no new inconsistencies arise, not even under time pressure or with frequently changing teams.
REST and OpenAPI patterns, the essentials at a glance
Resource design
Nouns, plural, hierarchies via path. Actions as sub-resources (POST /orders/{id}/cancellation), never verbs in the path.
Status codes & errors
Precise status codes instead of generic 200/500. Errors per RFC 9457 with application/problem+json and a violations array.
Pagination & idempotency
Cursor pagination for stable navigation. Idempotency-Key header for retry-safe POST requests.
OpenAPI & versioning
Schema first, $ref-based reuse. URL path versioning with a Sunset header for deprecation.
11. FAQ: REST and OpenAPI patterns for production-grade APIs
1What is the difference between PUT and PATCH?
application/merge-patch+json (RFC 7396) is the recommended content type, only the fields sent are updated.2Why no verbs in REST URLs?
POST /orders/{id}/cancellation is unambiguous, POST /cancelOrder is not.3401 vs. 403, what is the difference?
4What is RFC 9457?
application/problem+json. Reduces custom error documentation and is machine-readably distinguishable.5Cursor vs. offset pagination?
6When do I need an idempotency key?
7Schema-first vs. code-first?
8How do I communicate deprecation?
Deprecation: true header plus Sunset: <date> header plus a link to the successor version. At least 6 months lead time. Link the changelog and migration guide.9Which content types are standard?
application/json for responses, application/problem+json for errors, application/merge-patch+json for PATCH. Always specify charset=UTF-8 and Content-Type in the response.