Skip to main content
Architecture9 min readMarch 3, 2026

API Design Best Practices That Survive Production

API design best practices aren't just about clean URLs — they're about creating interfaces that are predictable, resilient, and easy to evolve. Here's what actually matters in production.

James Ross Jr.

James Ross Jr.

Strategic Systems Architect & Enterprise Software Developer

APIs Are Forever (or Close Enough)

When you ship a public API, you're making a contract. Clients — internal teams, partners, third-party developers — will build on top of that contract. Changing it later means breaking their code, coordinating migrations, and managing multiple versions simultaneously. This is expensive.

The decisions you make during API design have an outsized effect on how much pain you're managing two years from now. Most APIs I've audited have the same handful of problems: inconsistent error structures, no versioning strategy, opaque pagination, authentication that creates friction, and documentation that lags so far behind the implementation it's misleading. All of these are preventable.

Here's what actually matters.


Resource Design: Think Nouns, Not Verbs

REST APIs model resources, not procedures. The URL identifies what you're acting on; the HTTP method identifies what you're doing to it.

Good:

GET    /orders           → list orders
POST   /orders           → create an order
GET    /orders/{id}      → get a specific order
PATCH  /orders/{id}      → update an order
DELETE /orders/{id}      → cancel/delete an order

Avoid:

POST /getOrders
POST /createOrder
POST /updateOrderStatus
POST /cancelOrder

The POST-everything style is common in older RPC-style APIs and JSON-RPC interfaces. It's not inherently wrong, but it abandons the semantic clarity that HTTP methods provide — and that clients use to understand what to expect.

Nested Resources

Use nesting for resources that only exist in the context of a parent:

GET  /orders/{orderId}/items
POST /orders/{orderId}/items
GET  /orders/{orderId}/items/{itemId}

Don't nest more than two levels deep. GET /users/{id}/orders/{orderId}/items/{itemId}/reviews is technically correct and practically confusing. Beyond two levels, consider flattening: GET /items/{itemId}/reviews.


Versioning: Make a Decision and Commit to It

Every API that will have consumers needs a versioning strategy. The three common approaches:

URL versioning: /api/v1/orders, /api/v2/orders. Explicit, easy to route, easy to understand. The downside: clients have to update their URLs when you version. This is the approach I use most often because it's the most explicit.

Header versioning: Accept: application/vnd.myapp.v2+json. Clean URLs, but requires clients to set custom headers and makes version debugging harder.

Query parameter versioning: /api/orders?version=2. Simple but the least RESTful and easiest to forget.

The right choice depends on your consumers. Internal APIs where you control all clients can use any approach. Public APIs with external consumers benefit from URL versioning because it's visible in logs and easy to document.

Commit to the strategy before you ship v1. Once consumers are building on /api/orders, adding versioning is painful.


Error Handling: Be Specific and Consistent

Inconsistent error handling is the fastest way to make your API a frustration. I've worked with APIs where a 404 meant "resource not found," a 400 returned a plain string, and a 500 returned an HTML error page. Clients had to write special-case handling for every error type.

Pick a consistent error structure and use it everywhere:

{
  "error": {
    "code": "ORDER_NOT_FOUND",
    "message": "No order found with the provided ID.",
    "field": null,
    "requestId": "req_1a2b3c4d5e"
  }
}

Key principles:

  • Use appropriate HTTP status codes (don't return 200 with an error in the body)
  • Include a machine-readable error code for programmatic handling
  • Include a human-readable message for debugging
  • Include a requestId so clients can report specific failures to your support team
  • For validation errors, include field-level errors so clients can display inline error messages

HTTP Status Code Usage

  • 200 OK — successful GET, PATCH, PUT
  • 201 Created — successful POST that creates a resource; include Location header pointing to the new resource
  • 204 No Content — successful DELETE or action with no response body
  • 400 Bad Request — malformed request, validation errors
  • 401 Unauthorized — authentication required or token invalid
  • 403 Forbidden — authenticated but not authorized for this action
  • 404 Not Found — resource doesn't exist
  • 409 Conflict — state conflict (duplicate, version mismatch)
  • 422 Unprocessable Entity — request is syntactically valid but semantically wrong
  • 429 Too Many Requests — rate limit exceeded
  • 500 Internal Server Error — your fault, not the client's

Pagination: Don't Return Unbounded Collections

Never return all records in a collection endpoint. A collection that works fine at 100 records will destroy your API and your database at 100,000.

Cursor-based pagination is the most scalable approach. Return a cursor pointing to the last item, and the client passes it back to get the next page:

{
  "data": [...],
  "pagination": {
    "cursor": "eyJpZCI6MTAwfQ==",
    "hasMore": true,
    "limit": 20
  }
}

Offset pagination (?page=3&limit=20) is simpler and familiar but has problems at scale: page drift when records are inserted or deleted, and expensive OFFSET queries that become slow at large offsets.

For most APIs that will have moderate-to-large datasets, start with cursor pagination. It's more work upfront but avoids painful migrations later.


Authentication: Don't Reinvent It

API authentication is a solved problem. Don't invent a custom scheme unless you have a specific reason.

For internal or partner APIs: JWTs with short expiry (15 minutes) and refresh tokens. Include the minimum necessary claims in the payload — don't put everything in the JWT; it bloats every request header and becomes a source of stale data when claims change.

For public APIs: OAuth 2.0 with the appropriate grant type. Authorization Code with PKCE for user-facing applications, Client Credentials for machine-to-machine.

For simple internal services: Static API keys stored server-side, rotatable on demand.

Regardless of the approach:

  • Always use HTTPS — never accept auth tokens over plain HTTP
  • Set appropriate token expiry and force rotation
  • Include scopes so clients only get the permissions they need
  • Return 401 (authentication) vs 403 (authorization) correctly — they mean different things

Documentation: Write It Like Your Support Inbox Depends on It

It does. The quality of your API documentation directly determines how many support questions your team fields, how many integration errors your partners make, and how long it takes developers to get productive.

Good API documentation includes:

Authentication instructions that are step-by-step, with examples.

Endpoint reference with every parameter documented, including type, whether it's required, and valid values. Include example request and response bodies for every endpoint — not generic templates, actual representative examples.

Error code reference listing every error code your API can return and what a client should do about each one.

Getting started guide that takes a new developer from zero to their first successful API call in 15 minutes or less.

Change log noting what changed in each API version.

Tools like OpenAPI/Swagger make the reference portion maintainable by generating it from code annotations. Use them. But don't confuse generated reference docs with actual documentation — the conceptual guides, authentication walkthrough, and error reference require human writing.


Rate Limiting: Protect Yourself and Be Transparent

Every API exposed outside your infrastructure needs rate limiting. Communicate limits in response headers:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1709510400

When a client exceeds the limit, return 429 Too Many Requests with a Retry-After header indicating when they can try again. This gives clients everything they need to implement backoff without guessing.


The API That Earns Trust

The common thread through all of these practices is predictability. A good API does what clients expect, fails in ways they can handle, and evolves in ways they can adapt to without breaking their code. That predictability is what makes an API trustworthy, and trust is what makes external developers build on your platform.

Design for the developer who will consume your API at 2am when a production issue is happening. If they can figure out what went wrong from your error response, you've done your job.


If you're designing a new API or auditing an existing one, I work with teams on API strategy and design. Let's schedule time to talk.


Keep Reading