devcrea.com
Web & Frontend

REST API Design Best Practices for Production Reliability

REST API design best practices for production: cursor pagination, idempotency keys, RFC 9457 errors, and versioning. Build APIs that hold up under load.

REST API Design Best Practices for Production Reliability

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-Key headers for state-changing requests
  • Versioning: Prefer the Accept/Content-Type header, with URI /v1 as a fallback
  • Rate limiting: Return HTTP 429 with a precise Retry-After header

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.

Advertisement

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 Location header 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.

Advertisement

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.

Advertisement

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.

Advertisement

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.

Advertisement

Frequently asked

Questions & answers

What's the difference between PUT and PATCH in a REST API?

PUT replaces the whole resource, so you need to send every field; omitted fields may be set to null. PATCH changes only the fields you include. Use PATCH for a partial update, such as changing one email address.

Should I put the API version in the URL or a header?

URL versioning, such as /v1/customers, is easy to route, cache, and debug, so many teams start there. Versioning through the Accept header keeps URLs clean but asks clients to manage the header. Choose one approach and use it consistently.

Which HTTP status code should I use for a validation error?

Return 400 Bad Request when the payload is malformed or can't be parsed. Return 422 Unprocessable Entity when the JSON is valid but violates a business rule. Don't return 200 with an error hidden in the body.

Should I use cursor-based pagination or offset pagination?

Use cursor-based pagination for data that grows or changes often. With offset pagination, new records inserted during paging can shift results and cause duplicates. Offset can be fine for small, static datasets when a simpler setup matters more.

How can I make a POST request idempotent?

Require an Idempotency-Key header, usually a UUID, for each POST request and store it with the response. If the same key arrives again, return the stored response without processing the request twice. That keeps network retries from creating duplicate charges.

Do REST and RESTful mean the same thing?

In everyday use, yes. REST is an architectural style; RESTful describes an API that follows its constraints, including statelessness and resource-based URLs. Developers often use the terms interchangeably.

Share Article