Skip to main content
Back to IntelligenceSoftware Engineering

API Design Principles That Hold Up Over Time

The decisions you make when designing an API affect its usability and maintainability for years. Here are the principles that have proven durable.

E
Explicor
4 min read

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.

More Intelligence

Software Engineering

Kubernetes for Developers: The Core Concepts

Kubernetes manages containerized applications at scale. Here is a clear explanation of what it does, why it exists, and the concepts you need to work with it.

5 min
Software Engineering

Git Workflows That Work for Teams

How teams structure Git history, manage branches, and coordinate changes at scale — comparing trunk-based development, Gitflow, and practical variations.

5 min