API Design Best Practices: How to Build APIs Developers Love

Practical API design best practices — resource naming, HTTP methods, status codes, error formats, pagination, filtering, idempotency, versioning, auth, rate limiting and docs.

By Danyal Ahmed - Data Scientist · · 5 min read

An API is a product whose users are developers. A well-designed API is predictable: once you've used two endpoints, you can guess how the third works. A badly designed one forces every consumer to read the docs for every call, handle a dozen error formats and discover edge cases in production.

These best practices focus on HTTP/JSON APIs, but most principles apply to any style (REST vs GraphQL vs gRPC).

Key takeaways

  • Consistency beats cleverness: the same naming, pagination, errors and auth everywhere.
  • Use nouns for resources, HTTP methods for actions and correct status codes.
  • Return structured, actionable errors with machine-readable codes.
  • Support pagination, filtering and idempotency from day one; they're painful to retrofit.
  • Plan versioning and deprecation before your first external consumer.

1. Model resources, not procedures

Use plural nouns and let HTTP methods express the action:

httpGET    /v1/invoices              # list
POST   /v1/invoices              # create
GET    /v1/invoices/{id}         # retrieve
PATCH  /v1/invoices/{id}         # partial update
DELETE /v1/invoices/{id}         # delete
GET    /v1/customers/{id}/invoices

Avoid verbs in paths like /getInvoices or /createInvoice. For genuine actions that don't map to CRUD, use a sub-resource or action endpoint consistently:

httpPOST /v1/invoices/{id}/send
POST /v1/invoices/{id}/void

Naming conventions: pick one casing (usually snake_case or camelCase for JSON fields, kebab-case for URL segments) and never mix. Use ISO 8601 for timestamps (2026-09-28T14:30:00Z) and always include the timezone.

2. Use HTTP methods and status codes correctly

Method Semantics Idempotent?
GET Read, no side effects Yes
POST Create or perform action No (make it idempotent with keys)
PUT Replace entirely Yes
PATCH Partial update Not guaranteed
DELETE Remove Yes

Status codes clients can rely on:

Code Use for
200 OK Successful read/update
201 Created Resource created (include a Location header)
202 Accepted Async processing started
204 No Content Success with no body (e.g., delete)
400 Bad Request Malformed request / validation error
401 Unauthorized Missing or invalid authentication
403 Forbidden Authenticated but not allowed
404 Not Found Resource doesn't exist (or isn't visible to caller)
409 Conflict State conflict (duplicate, version mismatch)
422 Unprocessable Content Semantically invalid input (if you distinguish from 400)
429 Too Many Requests Rate limited (include Retry-After)
5xx Server-side failures

Never return 200 OK with {"error": ...} in the body. It breaks every client's error handling.

3. Design a consistent error format

Every error, from every endpoint, should share one structure:

json{
  "error": {
    "type": "validation_error",
    "code": "invalid_email",
    "message": "The email address is not valid.",
    "field": "customer.email",
    "request_id": "req_8f2a91c"
  }
}
  • code is stable and machine-readable; clients branch on it.
  • message is human-readable and can change.
  • request_id lets support trace the exact request in your logs.
  • For multiple validation errors, return an array of field errors.

Consider the RFC 9457 "Problem Details" format if you want a standard.

4. Paginate every list endpoint

Unbounded lists eventually time out. Choose a pagination style:

  • Offset/limit (?limit=20&offset=40): simple, supports jumping to pages, but slow on large offsets and inconsistent when data changes between requests.
  • Cursor-based (?limit=20&starting_after=inv_123): stable and fast at any depth. It's the better default for APIs.
json{
  "data": [ ... ],
  "has_more": true,
  "next_cursor": "eyJpZCI6Imludl8xNDMifQ"
}

Set a sensible default limit and a maximum. Cursor pagination works well with indexed columns (how database indexes work).

5. Filtering, sorting and field selection

Keep query parameters predictable:

httpGET /v1/invoices?status=open&customer_id=cus_42&created_after=2026-01-01&sort=-created_at
  • Use consistent names for common filters (created_after, created_before).
  • Prefix - for descending sort.
  • Optionally support fields= or expand= to control payload size and embed related resources.

6. Make writes idempotent

Networks fail. Clients retry. Without protection, a retried POST /payments charges twice. Accept an idempotency key header:

httpPOST /v1/payments
Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7

The server stores the key with the result; a repeated request with the same key returns the original response instead of performing the action again. This one feature prevents a whole class of costly bugs. The same idea keeps AI agents and queue consumers safe.

7. Version deliberately

  • Put the major version in the path (/v1/) or a header. Path versioning is the most visible and simplest.
  • Additive changes (new fields, new endpoints, new optional parameters) are non-breaking. Make clients tolerate unknown fields.
  • Breaking changes (removing or renaming fields, changing types or semantics) require a new version or an opt-in mechanism.
  • Deprecate with notice: document timelines, send Deprecation and Sunset headers, and email active consumers.

8. Authentication and authorization

  • Use OAuth 2.0 / OIDC for user-delegated access and API keys or client credentials for server-to-server.
  • Only over HTTPS.
  • Scope tokens to the minimum permissions.
  • Enforce authorization on every request at the resource level. Checking that user A can read invoice B is where many real-world breaches happen (broken object-level authorization).

Background: authentication vs authorization.

9. Rate limiting

Protect your service and give clients predictable limits:

httpHTTP/1.1 429 Too Many Requests
Retry-After: 30
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 30

Document limits per plan. Algorithms and trade-offs: rate limiting algorithms.

10. Webhooks for events

If clients poll constantly to check for changes, offer webhooks:

  • Sign payloads (HMAC) so receivers can verify authenticity.
  • Include an event ID and type; deliver at least once and expect receivers to deduplicate.
  • Retry with exponential backoff; provide a dashboard or endpoint to replay failed events.

11. Documentation and developer experience

  • Publish an OpenAPI specification and generate reference docs from it.
  • Show copy-pasteable examples for every endpoint, in multiple languages if possible.
  • Provide a sandbox environment and test API keys.
  • Keep a changelog.
  • Measure time to first successful call for new developers. It's the best single DX metric.

API design review checklist

  • Resource names are plural nouns; casing is consistent
  • Correct methods and status codes; no 200-with-error
  • Single error format with stable codes and request IDs
  • All lists paginated with enforced max limit
  • Idempotency keys supported for non-idempotent writes
  • Versioning and deprecation policy documented
  • Object-level authorization on every endpoint
  • Rate limits with 429 and Retry-After
  • OpenAPI spec and examples updated

FAQ

Should I use PUT or PATCH for updates?

Use PATCH for partial updates, which is what most clients want, and PUT only when clients send the complete resource representation. Be consistent and document the semantics.

Should an API return 404 or 403 for resources the user can't access?

Returning 404 avoids revealing that a resource exists, which is often preferable for private resources. Use 403 when the resource's existence isn't sensitive and you want to tell the client it lacks permission.

How do I handle long-running operations?

Return 202 Accepted with a job resource (/v1/jobs/{id}) the client can poll, or notify via webhook when it completes. Don't hold HTTP connections open for minutes.

Is HATEOAS necessary for a REST API?

Most practical APIs don't implement full hypermedia controls, and they're still widely called REST. Consistent resource design, clear documentation and good tooling matter more to API consumers.