Backend · August 14, 2026 · 9 min read
REST API Design: Decisions That Age Well
Resource naming, error contracts, pagination, idempotency and versioning: the choices that are cheap now and expensive to change later.
An API is a public contract, even when the only consumer is your own frontend. Clients get built against it, and mistakes become permanent the moment they ship. These are the decisions I now make deliberately at the start.
Model resources, not actions
URLs should name things. HTTP methods supply the verb.
GET /invoiceslists,POST /invoicescreatesGET /invoices/42reads,PATCH /invoices/42updates,DELETE /invoices/42removes- Use plural nouns and keep nesting shallow:
/invoices/42/itemsis fine, four levels deep is not
When an operation really is not CRUD, such as approving a leave request, model it as a sub-resource or an action endpoint instead of overloading a field update: POST /leave-requests/17/approval.
Use status codes honestly
Clients branch on the status code, so it needs to mean something.
200and201for success,204when there is nothing to return400for malformed input,422for valid syntax that fails validation401when unauthenticated,403when authenticated but not allowed404for a missing resource,409for a state conflict429for rate limiting, with aRetry-Afterheader
Returning 200 with { "success": false } forces every client to parse the body to learn what happened.
One consistent error shape
Pick a single error format and use it everywhere, including for unexpected failures.
{
"error": {
"code": "validation_failed",
"message": "The request contains invalid fields.",
"details": [
{ "field": "email", "message": "Must be a valid email address." }
],
"requestId": "req_8f2c1a"
}
}A stable machine-readable code lets clients react programmatically. The requestId lets you find the matching log line when someone reports a problem.
Paginate from day one
Unbounded list endpoints work until the table grows. Add pagination before you need it, because adding it later is a breaking change.
- Offset pagination (
?page=2&per_page=25) is simple and fine for admin tables - Cursor pagination (
?after=<token>) stays fast and stable on large or frequently changing data
Always enforce a maximum page size on the server.
Make unsafe operations idempotent
Networks retry. If a client sends POST /payments twice because a response was lost, you want one payment, not two. Accept an Idempotency-Key header, store the key with the result, and return the stored response on a repeat.
Validate at the boundary
Never trust input shape. Validate type, range and length at the edge, and keep the rules in one place. Whitelist the fields that may be set so a client cannot mass-assign role: "admin".
Plan for change
- Prefer additive changes: new optional fields rarely break anyone
- Never repurpose or change the meaning of an existing field
- When you must break something, version it (
/v2) and give consumers a deprecation window with a clear date - Document the contract with OpenAPI so the spec and the behaviour stay in sync
Do not forget the basics
- Authenticate every request and authorize per resource, not per route
- Rate limit by client, not only by IP
- Log request IDs, status codes and latency
- Return only the fields the client needs; never serialize a model wholesale
Good API design is mostly restraint. Boring, consistent conventions are what make an API pleasant to integrate and cheap to maintain.
- #REST
- #API Design
- #Backend
Keep reading