๐Ÿ› ๏ธ Developer Tools
ยท 8 min read

API Design Best Practices โ€” A Developer's Guide (2026)


The moment a client ships code against your API, every decision you made becomes permanent. Change a field name? Their app breaks. Remove an endpoint? Their deploy fails at 2 AM. Rename a status code? Their error handling silently stops working.

Good API design isnโ€™t about elegance โ€” itโ€™s about making the right thing obvious and the wrong thing hard. This guide covers everything: resource naming, HTTP methods, status codes, error formats, versioning, pagination, authentication, idempotency, rate limiting, and documentation.

If youโ€™re new to REST, start with What is a REST API? first. If youโ€™re evaluating whether REST is even the right choice, compare it with GraphQL and gRPC.

Resource-oriented URLs

URLs identify resources. Theyโ€™re nouns, not verbs.

โœ… Good
GET    /users
GET    /users/42
GET    /users/42/orders
POST   /users/42/orders

โŒ Bad
GET    /getUsers
POST   /createUser
GET    /getUserOrders?userId=42
POST   /deleteUser

Rules that save you from future regret:

  • Plural nouns for collections: /users, not /user
  • Nested resources for relationships: /users/42/orders (orders belonging to user 42)
  • No verbs in URLs โ€” the HTTP method is the verb
  • Lowercase, hyphen-separated: /order-items, not /orderItems or /OrderItems
  • No trailing slashes: /users, not /users/
  • Keep nesting shallow โ€” two levels max. Instead of /users/42/orders/7/items/3, use /order-items/3

For actions that donโ€™t map cleanly to CRUD, use a sub-resource:

POST /users/42/activate      โ†’ better than POST /activateUser
POST /orders/7/cancel         โ†’ better than POST /cancelOrder
POST /reports/generate        โ†’ for one-off operations

HTTP methods โ€” use them correctly

Each method has a specific meaning. Donโ€™t use POST for everything.

GET     /users          โ†’ List users (safe, cacheable)
GET     /users/42       โ†’ Get one user (safe, cacheable)
POST    /users          โ†’ Create a user (not idempotent)
PUT     /users/42       โ†’ Replace user 42 entirely (idempotent)
PATCH   /users/42       โ†’ Update specific fields (idempotent)
DELETE  /users/42       โ†’ Delete user 42 (idempotent)

Key distinctions people get wrong:

  • PUT replaces the entire resource. If you send a PUT without the email field, the email gets removed. PATCH only updates the fields you send.
  • GET must never change state. No GET /users/42/delete. Crawlers will follow that link and delete your data.
  • POST is the only non-idempotent method. Sending the same POST twice creates two resources. This matters โ€” see idempotency.
  • DELETE should be idempotent. Deleting something thatโ€™s already deleted returns 204, not 404.

Status codes

Donโ€™t return 200 OK for everything. Status codes tell clients what happened without parsing the body.

Code Name When to use
200OKSuccessful GET, PUT, PATCH
201CreatedSuccessful POST โ€” include Location header
204No ContentSuccessful DELETE โ€” no response body
301Moved PermanentlyResource URL changed permanently
304Not ModifiedCached response is still valid
400Bad RequestMalformed JSON, missing required fields
401UnauthorizedNo credentials or invalid token
403ForbiddenAuthenticated but not authorized
404Not FoundResource doesn't exist
409ConflictDuplicate entry, version conflict
422Unprocessable EntityValid JSON but failed validation
429Too Many RequestsRate limit exceeded โ€” include Retry-After header
500Internal Server ErrorUnhandled exception โ€” never expose stack traces
502Bad GatewayUpstream service failed
503Service UnavailableMaintenance or overloaded โ€” include Retry-After

The difference between 400 and 422 trips people up. Use 400 when the request is syntactically broken (malformed JSON). Use 422 when the JSON is valid but the data fails business rules (email already taken, amount is negative).

For the full list with examples, see the HTTP Status Codes cheat sheet.

Consistent error format

Every error response should use the same JSON envelope. Clients shouldnโ€™t have to guess the shape.

{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Request validation failed",
    "details": [
      {
        "field": "email",
        "message": "Must be a valid email address"
      },
      {
        "field": "age",
        "message": "Must be at least 18"
      }
    ],
    "requestId": "req_abc123"
  }
}

Rules for error responses:

  • Machine-readable code โ€” clients switch on this, not the message string
  • Human-readable message โ€” for developers reading logs
  • Field-level details for validation errors โ€” so forms can highlight the right fields
  • requestId in every response โ€” makes debugging support tickets possible
  • Never expose internal details โ€” no stack traces, no SQL queries, no file paths in production

For a deep dive on error handling patterns, error codes, and retry logic, see How to Handle API Errors.

Versioning

APIs change. Versioning lets you evolve without breaking existing clients.

The three main approaches:

URL path:     GET /v1/users          โ† most common, most visible
Header:       Accept: application/vnd.api+json;version=2
Query param:  GET /users?version=1   โ† least common

URL path versioning wins in practice because itโ€™s visible, cacheable, and easy to route. But there are real tradeoffs โ€” header versioning keeps URLs clean, and some teams use content negotiation for fine-grained control.

The full breakdown with migration strategies: REST API Versioning Strategies.

Pagination

Never return unbounded lists. A GET /users that returns 2 million rows will kill your server and the client.

Two common patterns:

Offset-based (simple, but skips items when data changes):

GET /users?page=2&limit=20

{
  "data": [...],
  "pagination": {
    "page": 2,
    "limit": 20,
    "total": 1847
  }
}

Cursor-based (stable, better for real-time data):

GET /users?cursor=eyJpZCI6NDJ9&limit=20

{
  "data": [...],
  "pagination": {
    "nextCursor": "eyJpZCI6NjJ9",
    "hasMore": true
  }
}

Cursor-based pagination is more reliable for large datasets because inserting or deleting rows doesnโ€™t cause items to be skipped or duplicated. Offset-based is simpler to implement and lets users jump to arbitrary pages.

For keyset pagination, infinite scroll patterns, and performance comparisons: Pagination Patterns Explained.

Authentication

Your API needs to know whoโ€™s calling. The main options:

  • API keys โ€” simple, good for server-to-server. Send in a header, never in the URL.
  • OAuth 2.0 / OIDC โ€” the standard for user-facing apps. Supports scopes, refresh tokens, third-party access.
  • JWT bearer tokens โ€” stateless, but you canโ€™t revoke them without extra infrastructure.
  • Session cookies โ€” fine for same-origin web apps, but donโ€™t work for mobile or third-party clients.

Always use HTTPS. Always. An API key sent over HTTP is a password on a postcard.

For a detailed comparison with implementation examples: API Authentication Compared.

Idempotency

Network failures happen. Clients retry. If a POST /payments gets retried, does the customer get charged twice?

Idempotent operations produce the same result regardless of how many times theyโ€™re called. GET, PUT, and DELETE are idempotent by definition. POST is not โ€” which is why payment APIs use idempotency keys:

POST /payments
Idempotency-Key: pay_req_abc123
Content-Type: application/json

{
  "amount": 5000,
  "currency": "usd",
  "customer": "cus_42"
}

The server stores the result keyed by pay_req_abc123. If the same key comes in again, it returns the stored result instead of processing a new payment.

This is critical for any operation involving money, inventory, or state changes that canโ€™t be undone. Full implementation guide: Idempotency in APIs.

Rate limiting

Without rate limiting, one misbehaving client can take down your entire API. At minimum:

  • Set per-client limits (e.g., 1000 requests/minute)
  • Return 429 Too Many Requests with a Retry-After header
  • Include rate limit headers so clients can self-throttle:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 247
X-RateLimit-Reset: 1716134400

The algorithms behind this (token bucket, sliding window, leaky bucket) matter more than youโ€™d think. A fixed-window counter has a burst problem at window boundaries that can double your effective limit. See How Rate Limiting Actually Works for the full breakdown.

Filtering, sorting, and field selection

Let clients ask for exactly what they need:

GET /users?status=active&role=admin          โ†’ filtering
GET /users?sort=-created_at,name             โ†’ sorting (- prefix = descending)
GET /users?fields=id,name,email              โ†’ sparse fieldsets

Conventions that work:

  • Use query parameters for filtering โ€” donโ€™t create separate endpoints for each filter
  • Prefix with - for descending sort
  • Support comma-separated values for multi-select: ?status=active,pending
  • Document which fields are filterable โ€” not every field should be

HATEOAS

HATEOAS (Hypermedia as the Engine of Application State) means including links in responses so clients can discover available actions:

{
  "id": 42,
  "name": "Alice",
  "status": "active",
  "_links": {
    "self": { "href": "/users/42" },
    "orders": { "href": "/users/42/orders" },
    "deactivate": { "href": "/users/42/deactivate", "method": "POST" }
  }
}

In theory, this makes APIs self-documenting and evolvable. In practice, most APIs skip it. Clients hardcode URLs anyway, and the added payload size isnโ€™t worth it for most use cases. GitHubโ€™s API is a notable exception that does it well.

If youโ€™re building a public API with many consumers, consider it. For internal APIs, good documentation is usually enough.

Webhooks

APIs arenโ€™t always request-response. When something happens asynchronously โ€” a payment completes, a build finishes, a user signs up โ€” webhooks push the event to the client instead of making them poll.

POST https://your-app.com/webhooks/payments
Content-Type: application/json
X-Webhook-Signature: sha256=abc123...

{
  "event": "payment.completed",
  "data": {
    "id": "pay_42",
    "amount": 5000,
    "currency": "usd"
  },
  "timestamp": "2026-05-19T14:30:00Z"
}

Always sign webhooks so receivers can verify they came from you. Always implement retry with exponential backoff for failed deliveries. For architecture patterns, security, and delivery guarantees: Webhook Architecture Patterns.

Documentation

An undocumented API doesnโ€™t exist. Developers wonโ€™t use what they canโ€™t understand.

What good API docs include:

  • Authentication โ€” how to get and use credentials
  • Every endpoint โ€” method, URL, parameters, request/response examples
  • Error codes โ€” what each error code means and how to fix it
  • Rate limits โ€” what the limits are and how to handle 429s
  • Changelog โ€” what changed and when

OpenAPI (Swagger) specs are the standard. They generate interactive docs, client SDKs, and can validate requests at runtime.

The difference between docs developers actually read and docs they ignore: API Documentation Developers Read.

API gateways

As your API grows, youโ€™ll need a layer in front of your services that handles cross-cutting concerns: authentication, rate limiting, request routing, logging, and SSL termination.

Thatโ€™s an API gateway. You can use managed solutions (AWS API Gateway, Kong, Apigee) or build your own for full control. If you want to understand the internals: Build an API Gateway From Scratch.

Design checklist

Before shipping an API endpoint, run through this:

  • URLs use plural nouns, no verbs
  • HTTP methods match the operation (GET reads, POST creates, etc.)
  • Status codes are specific (not just 200 and 500)
  • Error responses use a consistent JSON envelope with machine-readable codes
  • Pagination is implemented for all list endpoints
  • Authentication is required and uses HTTPS
  • Rate limiting is in place with proper headers
  • Idempotency keys are supported for non-idempotent operations
  • API is versioned
  • Every endpoint is documented with examples

Where to go from here

This guide covers the principles. The cluster articles go deep on each topic:

And the foundational references:

  • What is a REST API? โ€” the basics if youโ€™re just getting started
  • What is GraphQL? โ€” when REST isnโ€™t the right fit
  • What is gRPC? โ€” for high-performance service-to-service communication
  • How Rate Limiting Actually Works โ€” token bucket, sliding window, and leaky bucket algorithms
  • HTTP Status Codes Cheat Sheet โ€” the complete reference