Skip to main content

API Design

Every lesson in this module has covered one piece of an API in isolation — what it is, what sits in front of it, how it shapes data, how it stays safe under retries and load. Good API design is what happens when those pieces are chosen deliberately and made to work together, instead of accumulating ad hoc as a service grows. It's less about any single rule and more about a handful of habits that keep an API predictable to use and safe to evolve.

Model resources, not actions

A REST API (see REST vs. GraphQL) reads clearly when URLs name things and HTTP methods name the verbPOST /orders, not POST /createOrder; DELETE /orders/42, not POST /deleteOrder. This isn't a stylistic preference: once resources are nouns, the method/status-code vocabulary from APIs does real work, or plugs into HTTP-native caching the way REST vs. GraphQL described. An API that instead exposes a pile of verb-named endpoints (/getUser, /updateUserEmail, /deactivateUser) throws that consistency away and forces every client to learn each endpoint's behavior individually instead of relying on a shared convention.

Design for the request lifecycle, not just the happy path

Pulling together the rest of this module into one request's journey is a useful way to see how the pieces actually compose:

System Design Lab

Auth and rate limiting typically happen at an API Gateway before a request ever reaches business logic; idempotency handling wraps the mutation itself. A well-designed API is one where every request that could plausibly go wrong — unauthenticated, over quota, a retried duplicate — has an explicit, well-understood path, not just the successful one.

Version deliberately, before you need to

Every API contract will eventually need to change in a way that breaks existing clients — a field renamed, a response shape restructured. Because APIs are a contract, that change can't just happen to everyone at once; it needs a versioning strategy decided before the first breaking change is needed, not improvised in the middle of one:

StrategyExampleTradeoff
URL path/v2/ordersExplicit and cacheable, but "clutters" the URL and encourages permanent duplication of endpoints
HeaderAccept: application/vnd.api+json;version=2Keeps URLs clean, but harder to test in a browser and easy for clients to omit by accident
Query parameter/orders?version=2Simple to add, but easy to forget and awkward to treat as part of the resource's identity

None of these is a universally "correct" choice — the practical point is picking one deliberately and applying it consistently, so that a breaking change has a defined, non-improvised path to ship without cutting off every existing client at once.

Paginate anything that can grow without bound

An endpoint like GET /orders that returns all of a user's orders works fine in a demo and becomes a real problem the moment a heavy user has fifty thousand of them — an unbounded response is both a performance and a memory problem for both sides. Cursor-based pagination (?after=<opaque_cursor>&limit=50) is generally preferred over offset-based (?offset=1000&limit=50) for anything backed by a frequently-changing dataset, since offset-based pagination can skip or repeat items if rows are inserted or deleted between page requests — a subtle correctness bug that's easy to miss until it shows up as a real user complaint.

Keep error responses structured and consistent

A client needs more than a bare status code to handle an error well — a consistent JSON error body (a machine-readable error code, a human-readable message, and ideally which field caused a validation failure) lets client code branch on error.code reliably instead of parsing prose. Consistency matters more than the exact shape chosen: an API where every endpoint's error response has a different structure forces every integration to special-case each one.

Why this matters in an interview

API design questions reward showing that you treat the API surface as a deliberate artifact, not an incidental byproduct of the implementation: naming resources as nouns, planning a versioning strategy up front, pagination for anything unbounded, and a consistent error contract. Tying it back to the request-lifecycle diagram above — where auth, rate limiting, and idempotency each have an explicit place — is a strong way to show the whole module clicking together into one coherent answer, rather than as eight disconnected facts.

Designing an explicit versioning strategy from day one: pros and cons

Pros

  • Breaking changes ship without cutting off every existing client at once
  • Clients can opt into a new version on their own schedule
  • Makes the API's evolution predictable instead of ad hoc

Cons

  • Maintaining multiple live versions is real ongoing engineering cost
  • Requires discipline to actually deprecate and retire old versions
  • Adds a small amount of upfront design overhead before the first breaking change is even needed

Further Reading

Saved locally in your browser — visible in the sidebar as you go.