If a connection drops during a checkout POST request, the client may never learn whether the payment went through. A retry can then charge the customer twice. Building a REST API for production means thinking beyond basic CRUD: you need to handle concurrent requests, network failures, and changing data.
- Pagination: Cursor-based pagination avoids data drift under concurrent load
- Error handling: Use RFC 9457, Problem Details for HTTP APIs
- Idempotency: Use
Idempotency-Keyheaders for state-changing requests - Versioning: Prefer the
Accept/Content-Typeheader, with URI/v1as a fallback - Rate limiting: Return HTTP 429 with a precise
Retry-Afterheader
Resource Naming and URL Structure
Plural Nouns Over Verbs
Endpoints represent resources. Let HTTP methods say what happens to those resources, and use plural nouns in your URLs. Verb-based routes make the API harder to navigate and keep consistent.
Instead of endpoints like /getCustomers or /createOrder, use /customers. A GET request retrieves resources from that endpoint; a POST request creates one. Keeping names plural gives the API a consistent pattern.
The One-Level Nesting Rule
Avoid mapping your database relationships directly onto URLs. Deeply nested paths like /customers/123/orders/456/items/789 become hard to read and easy to break.
Keep nesting to one level. If you have an item's unique identifier, fetch it directly through /items/789. Use a nested path when it shows direct containment, such as listing a user's orders at /customers/123/orders.
HTTP Methods and the Reality of CRUD
Mapping Actions to HTTP Verbs Effectively
HTTP methods give clients a clear way to express intent. POST creates a resource, while GET retrieves data without changing it.
PUT and PATCH serve different purposes. PUT replaces a resource, so the client sends the full payload. PATCH changes only the fields supplied. To update an email address, use PATCH: a PATCH /customers/123 containing only {"email": "new@site.com"} leaves the other fields alone. Sending that same body with PUT could set the name, phone, and address to null. Using POST for updates or GET for changes also creates problems for caching and intermediary proxies.
Choosing the Exact 2xx, 4xx, and 5xx Status Codes
Don't return 200 OK while hiding an error in the JSON body. Status codes let clients and load balancers understand the result without parsing the response body.
- 201 Created: Return this when a POST creates a resource, with a
Locationheader pointing to its new URI. - 202 Accepted: Use this when a valid request has been accepted for asynchronous processing but is not yet complete.
- 400 Bad Request: The payload is malformed or fails validation.
- 422 Unprocessable Entity: The JSON is well formed but violates a business rule.
- 404 Not Found: Use this for missing resources, not unauthorized access.
Standardizing Error Responses with RFC 9457
Moving Beyond Generic Error Messages
An unhandled exception or a response like {"error": "Something went wrong"} leaves client developers guessing. A consistent error format makes failures easier to diagnose.
RFC 9457, Problem Details for HTTP APIs, provides a standard way to describe those failures across your API.
Structuring the Problem Details JSON Envelope
An RFC 9457 response gives clients structured information they can act on. Include these fields:
- type: A URI reference identifying the problem type, such as
https://example.com/probs/out-of-credit. - title: A short, readable summary.
- status: The HTTP status code returned by the origin server.
- detail: An explanation specific to this failure.
- instance: A URI reference identifying this particular occurrence.
Serve the response with a Content-Type: application/problem+json header so clients can recognize it as a structured problem. A validation failure might look like this:
{
"type": "https://api.example.com/probs/validation",
"title": "Your request parameters did not validate.",
"status": 422,
"detail": "The field 'email' must be a valid email address.",
"instance": "/customers/123"
}
Pagination: Why Cursor Beats Offset in Production
The Data Drift Problem in Offset Pagination
Offset pagination (?limit=20&offset=40) counts rows from the beginning of a result set. That can cause problems when records are added while someone is paging through it.
Suppose a user loads the first 20 records. Before they request the next page, five new records appear at the top. Their next request (offset=20) now starts at a shifted position, so records from page 1 show up again on page 2. That's data drift.
Implementing Scalable Cursor-Based Filtering and Sorting
Cursor-based pagination uses a position in the ordered results instead of a row count. The client passes a cursor that tells the API where to continue.
The client requests ?limit=20. Your API returns the records and a next_cursor token, often based on a timestamp or UUID and encoded in base64. The client includes that token in the next request (?limit=20&cursor=XYZ). The database can then seek to the indexed position and fetch the next batch. The query's work stays steady as the client moves through the results:
SELECT * FROM orders
WHERE created_at < :cursor
ORDER BY created_at DESC
LIMIT 20;
Seeking by an indexed value avoids the shifts caused by new records and is often much faster on large datasets than OFFSET, which must walk past skipped rows.
Idempotency Keys in POST Requests
Preventing Duplicate Transactions
Network connections fail. A client sends a POST request to charge a card, the server processes it, and the response gets lost. The client sees a timeout and retries. Without protection, the customer may be charged twice.
Idempotency makes repeated requests with the same intent produce the same result as a single request.
Handling Retries with Idempotency Keys
Require an Idempotency-Key header, usually containing a UUID, on every POST and PATCH request.
Store the key with the response to the original request. When the same key appears again, return that response without running the business logic a second time. This prevents duplicate processing when gateways time out or clients retry.
Handle two details carefully. Give stored keys a finite TTL; 24 hours is a common choice. And if an existing key arrives with a different request body, return 409 Conflict instead of the stored response. Reusing a key for different data usually points to a client bug. Payment platforms such as Stripe use this pattern to make retries safe.
Authentication, Security, and CORS
Enforcing HTTPS and Bearer Tokens
Use established authentication approaches such as OAuth2 or JSON Web Tokens (JWT) instead of inventing your own.
Clients should send credentials in the standard Authorization: Bearer <token> HTTP header. Configure the API gateway to require TLS 1.2 or higher, and reject unencrypted HTTP requests at the edge instead of redirecting them.
Why Credentials Never Belong in Query Strings
Putting API keys or session tokens in a URL (?api_key=secret) exposes them unnecessarily.
URLs can end up in browser history, proxy logs, load balancer access logs, and APM tools. A credential in a query string may therefore be visible in many places. Keep credentials in HTTP headers or the encrypted request body.
Versioning Strategies and Deprecation Policies
URL Versioning vs. Accept Header
APIs change, and some changes break existing clients. Two common ways to manage that are URL versioning and header versioning.
URL versioning (/v1/customers) is straightforward to route, cache, and debug. Content negotiation through the Accept header (Accept: application/vnd.company.v1+json) keeps version numbers out of URIs, but clients have to manage the header. Choose one approach and use it consistently.
Sunsetting APIs Gracefully
Give clients time and clear signals before retiring an API version.
Use the standard Deprecation HTTP header to identify endpoints being deprecated. Add a Sunset header with the exact time they will stop working. Clients can detect these headers and alert their developers before the cutoff.
Rate Limiting, Caching, and Observability
Protecting the Gateway with 429 Too Many Requests
A runaway client loop can overwhelm a backend. Apply rate limits at the API gateway, before requests reach the application servers, whether those servers run behind a Docker alternative or on a managed platform.
When a client reaches its limit, return 429 Too Many Requests with a Retry-After header. The header tells the client how long to wait before retrying, helping reduce repeated requests during a traffic spike. Base limits on measured capacity: database load testing can show where the backend starts to struggle.
Keeping OpenAPI Documentation in Sync
Documentation becomes misleading when it falls behind the code. Manual wiki updates are easy to miss after an API change.
Generate OpenAPI (Swagger) specifications from code annotations and routing logic. If you use JavaScript, keeping your Node.js version current also helps generator plugins work across builds. This keeps interactive documentation, client SDKs, and API behavior aligned as you deploy changes.
These practices work together. Cursor pagination keeps reads consistent, idempotency keys protect writes, and RFC 9457 makes failures easier to understand. Start with those three to prevent many of the incidents teams encounter when launching their first public API.
