An API is a contract. Once published, it is difficult to change without breaking things that depend on it. This makes API design unusually important: decisions made early tend to persist long after the original context has changed.
The principles here are not tied to a specific style (REST, GraphQL, gRPC) — they apply broadly to any interface between systems.
Design for the consumer, not the producer
The most common mistake in API design is building the API around the internal structure of the service rather than the needs of its consumers. If your database has a user_preferences table and an account_settings table, that does not mean your API should expose those as separate endpoints. The consumer may need both together in a single call.
Before designing endpoints or operations, ask: who will use this, what will they need to do, and what information do they need each time?
Use clear, consistent naming
Naming is hard, and inconsistent naming is worse than any particular choice. If you use created_at on one resource and createdDate on another, consumers have to look up the schema for every resource. Pick a convention and apply it everywhere.
Good names are:
- Self-descriptive without reference documentation
- Consistent with domain language the consumer uses
- Unambiguous (avoid names like
data,info,value)
Avoid chatty APIs
A chatty API requires many round trips to accomplish a single task. Fetching a page that shows a list of articles with their authors requires first fetching the articles, then fetching each author separately — if the API is designed poorly.
Design APIs to return the information consumers actually need in a single request. This may mean designing endpoints around use cases rather than data models. It is acceptable to have some redundancy in what a response returns if it eliminates network round trips.
Version from day one
Even if you plan not to make breaking changes, build versioning into your API from the start. The most common approach for HTTP APIs is URL versioning (/v1/users), which is explicit and easy to understand. Header-based or accept-type versioning are alternatives with their own trade-offs.
A version is a promise: consumers on v1 can continue using v1 as long as it is supported, even after v2 is released with breaking changes.
Error responses need as much care as success responses
Consumers need to handle errors. Error responses should include:
- A stable, machine-readable error code (not just an HTTP status code)
- A human-readable message explaining what went wrong
- Where relevant, which field or parameter caused the error
- A unique request ID for debugging
A vague 400 Bad Request with no body is nearly useless to a consumer trying to debug why their request failed.
Pagination is not optional for collections
Any endpoint that returns a list of items should support pagination. Returning unbounded lists is dangerous — someone will eventually have 100,000 items, and your API will send them all in one response.
Cursor-based pagination is generally superior to offset pagination for large or frequently changing datasets:
- Cursor:
GET /articles?after=cursor_xyz— stable as new items are added - Offset:
GET /articles?page=5&per_page=20— page 5 shifts as new items are added
Be explicit about nullability and optionality
Consumers need to know whether they should expect a field to be present, or whether it might be absent or null. This should be explicit in your documentation (and ideally enforced by schema validation on both sides).
A field that is sometimes present and sometimes absent, with no documentation, forces consumers to write defensive code for every field they use.
Rate limiting and quotas should be communicated in headers
If you rate limit API requests (and you should), communicate the limits and current state in response headers:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 982
X-RateLimit-Reset: 1717000000
This allows consumers to implement intelligent backoff strategies rather than blindly hitting your API until they get a 429.
Deprecation needs a timeline
When you deprecate part of your API, communicate:
- What is being deprecated
- What consumers should use instead
- When the deprecated part will be removed
A deprecation without a timeline is easy to ignore. A deprecation with a specific end-of-life date — communicated in documentation, in headers, and via email if you have contact information — gives consumers a concrete deadline to plan for.
Summary
Good API design optimizes for the consumer's needs, uses consistent naming, minimizes round trips, builds versioning in from the start, provides useful errors, paginates collections, documents nullability explicitly, communicates rate limits in headers, and handles deprecation with a timeline. These principles are relatively stable regardless of the specific API style or transport protocol used.