Skip to content

Guide

API design best practices

API design best practices are the conventions that make an API predictable, hard to misuse and safe to change: consistent names and resources, clear errors, stable pagination, idempotent writes and a plan for evolution. Good design matters more for APIs than for most code, because once clients depend on a decision it is expensive to undo.
By the Releo team · Updated · 8 min read

Design principles first

Three principles sit under every specific rule in this guide:

  • Consistency beats cleverness. A developer who has learned one endpoint should be able to guess the next. Pick conventions and apply them everywhere.
  • Design for the caller. Model what clients need to do, not how your database is laid out. Internal table names and join structures do not belong in a public contract.
  • Every decision is a commitment. Anything you expose, clients will depend on, including behavior you never documented. Expose less, deliberately.

A useful test for any design decision: imagine explaining it in the reference docs to a developer who has never seen your codebase. If the explanation needs the words "except" or "unless", the design probably needs another pass.

Writing the contract before the code, the API-first approach, makes these principles easier to keep: reviewers argue about an OpenAPI document before anyone has built the wrong thing.

Naming and resources

  • Use nouns for resources and let HTTP methods supply the verbs: POST /invoices, not POST /createInvoice.
  • Use plural collection names: /invoices and /invoices/{id}.
  • Nest only for true ownership, and only one level deep: /customers/{id}/invoices is clear, four levels of nesting is not.
  • Pick one case style for JSON fields (snake_case or camelCase) and never mix them.
  • Use stable, opaque IDs, ideally prefixed by type (inv_123), and never expose auto-incrementing database keys that leak volume and invite enumeration.
  • Use ISO 8601 timestamps in UTC and integer minor units with an explicit currency for money. Ambiguous units are a classic source of silent bugs.
  • For actions that do not fit CRUD, a sub-resource such as POST /invoices/{id}/void is clearer than overloading PATCH.

Respect HTTP method semantics. GET must be safe (no side effects). GET, PUT and DELETE are defined as idempotent, so repeating them leaves the server in the same state. POST is not, which is why it needs the idempotency keys described below.

Errors clients can act on

Use HTTP status codes for the category and a structured body for the detail. 400 for malformed input, 401 for missing or invalid credentials, 403 for insufficient permission, 404 for a missing resource, 409 for a conflict, 422 for semantically invalid input, 429 for rate limits, and 5xx only for server faults.

For the body, the Problem Details format (RFC 9457) is a good standard to adopt:

HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json

{
  "type": "https://api.example.com/errors/invalid-currency",
  "title": "Unsupported currency",
  "status": 422,
  "detail": "Currency XYZ is not supported for this account.",
  "instance": "/invoices"
}

Give every error a stable machine-readable code that clients can branch on, separate from the human message, and treat those codes as part of the contract. Changing an error code or status is a breaking change for any client that handles it. Include a request ID in every response so support can trace a failure.

Pagination, filtering and sorting

Paginate every list endpoint from day one; adding pagination later changes the response for every client. Two common approaches:

  • Offset pagination (?offset=40&limit=20) is simple and supports jumping to a page, but it gets slow on large tables and skips or repeats items when data changes between requests.
  • Cursor pagination (?limit=20&starting_after=inv_123 or an opaque cursor) is stable under concurrent writes and scales well. It is the better default for most APIs.

Document the default and maximum page size, and the default sort order. Defaults are part of the contract: changing them quietly changes what clients receive. Use consistent query parameter names for filters across every collection.

Idempotency and safe retries

Networks fail after the server has done the work but before the client hears back. If the client retries a POST, it may create a second payment. The standard fix is an idempotency key: the client sends a unique value with the request, and the server stores the result and returns it for any repeat with the same key.

POST /payments
Idempotency-Key: 3f7c9a2e-1b4d-4c8a-9e21-7d0f5b6a8c11
Content-Type: application/json

On the server, store the key together with a fingerprint of the request and the final response, scoped to the calling account. If a second request arrives while the first is still running, return a conflict rather than processing it twice. Reject a reused key that arrives with a different request body, and document how long keys are retained. See idempotency for the concept and webhooks for the same problem in reverse, where your consumers must handle duplicate deliveries.

Versioning and evolvability

Design for change before you need it:

  • Prefer additive change. New endpoints, new optional parameters and new response fields can usually ship without a new version.
  • Tell clients to ignore unknown fields and to tolerate unknown enum values, and say so in the docs. That single rule makes most additive changes safe.
  • Return objects, not bare values. { "total": { "amount": 1200, "currency": "USD" } } can grow; "total": 12 cannot.
  • Choose a versioning scheme early and apply it consistently. See API versioning strategies.
  • Plan retirement. Publish a support window and use standard deprecation signals. See how to deprecate an API.
  • Check compatibility automatically. An OpenAPI diff in CI catches structural breaks, and traffic replay testing catches behavioral ones.

Keeping design consistent at scale

A few more conventions pay off across a whole API surface: return the full updated object from every write so clients do not need a follow-up read, accept partial updates with PATCH and document how null differs from an omitted field, and expose list filters consistently, such as created[gte] on every collection that has a creation date. Small, boring consistency compounds; every exception is something a developer has to look up.

Rules only help if they are enforced. Write a short style guide, lint OpenAPI documents in CI against it, run design reviews for new public endpoints, and publish reusable components for errors, pagination and common objects. Pair this with security and documentation standards so that every new endpoint ships with auth rules and examples, not just a schema.

Even well designed APIs eventually change in ways that break someone. When they do, the cost lands on consumers, who must find and update every call site. Releo automates that consumer side with migration pull requests verified against replayed traffic.

Frequently asked questions

What are API design best practices?

They are conventions that make APIs predictable and safe to evolve: noun-based resource names, consistent field casing, correct HTTP methods and status codes, structured errors, cursor pagination, idempotency keys for writes, and a clear versioning and deprecation policy.

Should REST API endpoints be plural or singular?

Plural is the most common convention, for example /invoices for the collection and /invoices/{id} for one item. Either works if you apply it consistently across the whole API.

What is the best pagination method for an API?

Cursor-based pagination is the best default for most APIs because it stays stable when data changes and performs well on large datasets. Offset pagination is simpler and allows jumping to arbitrary pages, which suits small, rarely changing collections.

Coming soon

Ship API changes without breaking your customers.

Releo is in private beta. Join the waitlist for early access.