Web development
API design principles that survive growth
The API design principles that keep an API alive: resources, single-meaning status codes, machine-readable errors and a versioned contract.
Key points
- An API grows better when resources are nouns and verbs live in the HTTP methods.
- A status code must have one meaning: 200 for everything makes an error indistinguishable from a success.
- RFC 9457, July 2023, defines a standard error format: application/problem+json.
- Cursor pagination stays stable when data changes; offset pagination skips or duplicates rows.
API design principles that survive growth come down to five: resources instead of verbs, HTTP codes with a single meaning, machine-readable errors, pagination and idempotency designed up front, a versioned contract. Everything else is detail you can change without breaking clients.
Resources, not verbs
A REST API exposes nouns: /orders, /orders/42, /orders/42/items. The action lives in the HTTP method, not the path. POST /orders/42/cancel is acceptable when cancellation is not a field update, but GET /getOrders is a design error: GET must be safe and repeatable, and a verb in the path invites using it for everything. The comparison with server actions is in route handlers and server actions.
HTTP codes are not decoration
RFC 9110, HTTP Semantics, June 2022, defines what methods and statuses mean. Use 201 for a created resource, 204 for success without a body, 400 for a malformed request, 401 for missing identity, 403 for a known identity without permission, 404 for a missing resource, 409 for a state conflict, 422 for syntactically valid data that is not acceptable, 429 for too many requests. Returning 200 with {"error": "not found"} forces every client to read the body to learn what happened, and breaks caches, retries and monitoring.
Errors a machine can read
RFC 9457, Problem Details for HTTP APIs, July 2023, defines a standard error body with type, title, status, detail and instance, served as application/problem+json. A client can tell a validation error from a temporary failure without parsing the message text. The type field is a URI: put the documentation there, not a sentence for humans.
Pagination: cursor, not offset
?offset=40&limit=20 looks simple until a record is inserted or deleted: the next page skips a row or repeats it. An opaque cursor, for example ?after=eyJpZCI6NDJ9, marks a stable point in the ordering. Always return next_cursor and an explicit has_more: the client should not infer the end from the page length.
Idempotency and versioning
A POST repeated after a timeout can create two orders. The Idempotency-Key header, with a client-generated key kept for at least 24 hours, lets the server return the first response instead of running again. For versioning, the number in the URL (/v1/) is the most readable and the easiest to monitor; the version in a header works better with few controlled clients. Either way you need a deprecation date and a period of dual operation.
The contract before the code
OpenAPI 3.1.0, published on 15 February 2021, describes paths, schemas and responses in JSON Schema. Written before implementation, the contract lets you generate clients, documentation and conformance tests from one file. The command below validates the specification and checks that the endpoint answers: two checks that catch most breakages.
npx @redocly/cli lint openapi.yaml
curl -s -o /dev/null -w "%{http_code}\n" https://api.example.com/v1/orders
Security and personal data
Regulation (EU) 2016/679 of 27 April 2016, article 32, requires measures proportionate to the risk: strong authentication, encryption in transit, access logs and rate limits. Article 25 requires minimisation in the design: an endpoint that returns the whole user record to show a name is a design problem, not a configuration one. On the application side, access control belongs in middleware, not in the client; the model is described in authentication middleware.
Where these principles stop
An internal API called by one service can use RPC and gain nothing from REST. A real-time flow needs WebSocket or Server-Sent Events, where HTTP codes do not reach. And no principle fixes latency: if a response takes three seconds, perfect documentation will not save the experience.
Where to start
Frequently asked questions
REST or GraphQL?
It depends on the clients. REST is simpler to cache and monitor; GraphQL pays off when many clients ask for different data shapes and the cost of a controlled query is acceptable. For a small public API, REST remains the more predictable choice.
When should I use POST and when PUT?
PUT replaces a resource at a known path and is idempotent: repeating it produces the same state. POST creates a resource or starts an action and is not idempotent, unless you use an idempotency key.
What is the difference between 401 and 403?
401 means authentication is missing or invalid: the client must obtain credentials. 403 means the identity is known but lacks permission: repeating the request changes nothing.
Is versioning the API really necessary?
Yes, as soon as you have a client you do not update together with the server. The number in the URL is the most readable form; a header version creates less noise but needs more monitoring discipline.
How do I handle a validation error?
Reply 422 with an application/problem+json body listing the invalid fields. Do not return 200 with a message: the client cannot tell a success from a refusal.