Skip to main content

Command Palette

Search for a command to run...

API Design: Building Contracts That Last

Updated
12 min readView as Markdown
API Design: Building Contracts That Last

Systems Design

# Post What it covers
00 APIs & Communication: How Services Talk to Each Other How services talk to each other shapes everything about a system. Nine concepts covering REST, WebSockets, async patterns, and API gateways. (146 chars)
01 API Design: Building Contracts That Last ← you are here A great API is a contract that outlasts your code. Here are the principles that make APIs intuitive to consume, safe to evolve, and cheap to maintain. (154 chars)
02 REST APIs: Constraints That Create Benefits REST isn't just HTTP with JSON. It's an architectural style with specific constraints — and understanding them explains why REST APIs are designed the way they are. (166 chars)
03 Authentication vs Authorisation: Two Questions, Two Checks Authentication is who you are. Authorisation is what you're allowed to do. Confusing them is one of the most common security mistakes in system design. (153 chars)
04 Session vs Token Authentication: Stateful vs Stateless Identity Session auth stores identity on the server. Token auth encodes it in the token. Here's how each works, where each breaks, and how to choose. (144 chars)
05 OAuth 2.0 & OpenID Connect: Delegated Access and Federated Identity OAuth 2.0 lets users grant apps access without sharing passwords. OpenID Connect adds identity on top. Here's how both actually work. (137 chars)
06 JWT: What's Actually Inside the Token JWTs are everywhere in modern auth — and frequently misused. Here's exactly what a JWT contains, how the signature works, and what it doesn't protect. (153 chars)
07 WebSockets: Real-Time Bidirectional Communication HTTP is request-response. WebSockets are a persistent two-way channel. Here's how they work, when to use them, and what to watch out for at scale. (151 chars)
08 Long Polling, SSE & Webhooks: The Server-Push Spectrum Three patterns for server-push communication — long polling, server-sent events, and webhooks. Here's how each works and when to reach for each. (150 chars)
09 Sync vs Async Communication: The Architectural Fork Synchronous services couple tightly. Asynchronous services decouple — but add complexity. Here's how to reason about which your system needs. (147 chars)
10 API Gateways: One Entry Point, Every Cross-Cutting Concern An API gateway centralises auth, rate limiting, routing, and observability for all your services. Here's what it does, how it works, and when you need one. (158 chars)
11 APIs & Communication: Wrap-Up A complete recap of all ten API and communication concepts — REST, auth, JWT, WebSockets, webhooks, async patterns, and API gateways — and how they connect. (161 chars)

API Design: Building Contracts That Last

The problem

Your team ships a public API for the URL shortener platform. Developers integrate quickly — within a month, dozens of third-party tools are creating and managing short links through your API. Then a new requirement arrives: the response shape for GET /links/{id} needs to change. The destination field needs to become a nested object to accommodate metadata.

Simple change. Except it isn't. Every integration that destructures that response will break silently or loudly. You have no way to contact every developer who built against your API. Rolling back is not an option because other changes shipped in the same version. You're now maintaining two parallel response formats indefinitely, or issuing a deprecation that will anger your most engaged users.

This is what bad API design costs. Not at design time — at evolution time. The API that seemed fine when you shipped it becomes a trap the moment the underlying system needs to change.


The core idea

API design is the practice of defining interfaces between systems that are intuitive to consume, consistent in their behaviour, honest about their contracts, and safe to evolve over time. The decisions you make at design time — naming, structure, error handling, versioning — are decisions you live with for as long as anyone depends on your API.

Good API design is not primarily about aesthetics. It is about reducing friction for consumers and reducing the cost of change for maintainers.


The analogy: a well-designed power socket

A power socket has a standardised interface: specific pin shapes, specific voltage, specific grounding. You can plug in any compatible device without reading a manual, without knowing how the electrical grid works, and without calling the electricity provider. When the grid infrastructure changes, the socket interface stays the same — your devices keep working.

A well-designed API works the same way. Consumers plug in without reading implementation details. Changes to the underlying system don't break the interface. The surface area is minimal and predictable.

A poorly designed API is a proprietary socket — it works, but only for devices built specifically for it, in ways that make sense only to whoever designed it, with no guarantee that it will keep working after the next update.


How it works

Design for the consumer, not the implementation

The most common API design mistake is exposing the implementation rather than the domain. APIs that mirror internal database schemas, that use internal terminology unfamiliar to consumers, or that require understanding the implementation to use correctly are designed for the implementer.

A short link creation request should not look like:

POST /api/v1/tbl_links/insert
{
  "lnk_dest_url": "https://example.com/very-long-url",
  "lnk_ttl_seconds": 86400,
  "usr_id_fk": 4821
}

It should look like:

POST /links
{
  "destination": "https://example.com/very-long-url",
  "expires_in": "24h"
}

The first exposes database table names, column naming conventions, and internal foreign key structure. The second speaks the consumer's language. If the database schema changes, the first API breaks consumers. The second doesn't — it was never coupled to the schema.

Design principle: ask "what does the consumer want to do?" before asking "what does the implementation need?"

Naming conventions

Consistency in naming reduces cognitive load. Consumers shouldn't have to check documentation to know whether a field is createdAt, created_at, CreateTime, or date_created. Pick one convention for the entire API and never deviate:

  • URLs: lowercase, hyphen-separated (/short-links, /user-accounts)
  • JSON fields: snake_case (most REST APIs) or camelCase (common in JavaScript-heavy ecosystems) — pick one
  • Booleans: positive names (is_active, has_access) rather than negative (not_deleted, no_expiry)
  • Timestamps: ISO 8601 (2026-01-15T09:30:00Z) — timezone-aware, universally parseable
  • IDs: strings, not integers — integer IDs expose record counts, are sequential (guessable), and create problems at distributed-system scale

Resource naming and URL structure

URLs should identify resources (nouns), not actions (verbs):

Good:
  GET    /links           → list all links
  POST   /links           → create a link
  GET    /links/{id}      → get a specific link
  PATCH  /links/{id}      → update a link
  DELETE /links/{id}      → delete a link
  GET    /links/{id}/clicks → get clicks for a link

Avoid:
  POST /createLink
  GET  /getLink?id=123
  POST /deleteLink
  GET  /getLinkClicks?linkId=123

The action is expressed by the HTTP method. The URL identifies the thing being acted on. This isn't just convention — it's what makes HTTP caching, proxy behaviour, and standard tooling work correctly with your API.

Nested resources express relationships (/links/{id}/clicks) but should only be nested one level deep. Deep nesting (/users/{id}/organisations/{id}/teams/{id}/members) creates brittle URLs that couple the hierarchy into the API surface.

Request and response design

Be consistent about nulls vs omitted fields. Either always include fields with null values, or always omit absent fields. Mixing both means consumers need to handle both {"expires_at": null} and {} as "no expiry" — two representations of the same state.

Don't use response envelopes unless you need metadata. A common pattern wraps every response in {"data": {...}, "meta": {...}}. This adds no value for single-resource responses and forces consumers to always unwrap. Only use envelopes when the metadata (pagination, rate limit info) is genuinely useful to consumers.

Paginate all collection responses from day one. An endpoint that returns an unpaginated list will eventually have a list too large to return. Adding pagination after launch is a breaking change. Default to cursor-based pagination for APIs that need stable ordering under concurrent writes; offset-based pagination for simpler use cases.

GET /links?cursor=eyJpZCI6MTAwfQ&limit=20

{
  "links": [...],
  "next_cursor": "eyJpZCI6MTIwfQ",
  "has_more": true
}

Error design

Error responses are part of your API contract. Consumers handle errors in code — vague or inconsistent errors create defensive code that's hard to maintain.

A good error response tells the consumer three things: what went wrong (machine-readable code), why it went wrong (human-readable message), and what to do about it (optional but valuable):

HTTP 422 Unprocessable Entity

{
  "error": {
    "code": "INVALID_DESTINATION_URL",
    "message": "The destination URL is not a valid HTTPS URL.",
    "field": "destination",
    "docs_url": "https://docs.sho.rt/errors/INVALID_DESTINATION_URL"
  }
}

A machine-readable code means consumers can write if (error.code === 'INVALID_DESTINATION_URL') rather than parsing the message string. A field reference points consumers directly to the problem. A docs_url provides depth without cluttering the response.

Use standard HTTP status codes correctly:

  • 400 — the request is malformed or logically invalid
  • 401 — unauthenticated (no valid credentials)
  • 403 — authenticated but not authorised for this resource
  • 404 — resource doesn't exist (or you're hiding it intentionally)
  • 409 — conflict with current state (duplicate, version mismatch)
  • 422 — semantically invalid (valid JSON, invalid content)
  • 429 — rate limited
  • 500 — your fault, not theirs

Versioning

APIs change. Versioning is how you change them without breaking existing consumers.

URL versioning (/v1/links, /v2/links) is the most explicit approach — the version is visible in every request, easy to route at the infrastructure layer, and easy for consumers to understand. It's also the most work to maintain, since you're operating multiple complete API versions simultaneously.

Header versioning (API-Version: 2) keeps URLs clean but makes the version less visible — easy to forget, harder to test in a browser.

No versioning (breaking change aversion) — design the API carefully enough that breaking changes are never needed. Additive changes only: new fields, new endpoints, new optional parameters. Existing fields are never renamed, removed, or changed in type. This works until it doesn't, and when it doesn't, it hurts.

The practical recommendation: use URL versioning for your major public API, build additive-only discipline into your process, and treat a version bump as the costly escalation it is — not a routine release mechanism.

Idempotency

State-changing operations — create, update, delete — should be idempotent where possible: calling them multiple times with the same inputs produces the same result as calling them once.

For creation specifically, clients often can't know whether a previous request succeeded (network failure after the server processed it but before the response arrived). Idempotency keys solve this:

POST /links
Idempotency-Key: 7f3d2a1b-4e5c-4f8a-9b2d-1c3e5a7f9b0d

{
  "destination": "https://example.com"
}

The server stores the key and response. If the same key arrives again, the server returns the stored response rather than creating a duplicate. The client can safely retry without risk of duplicate creation. Stripe pioneered this pattern and it's now a standard expectation for any API handling money or critical operations.


The tradeoffs

Consistency vs flexibility. Rigid naming and structure conventions reduce the API's flexibility to model unusual domains. Some resources genuinely don't fit the noun/verb REST pattern cleanly — a calculate operation, a search with complex parameters, a batch endpoint. The right answer is to follow conventions where they fit and deviate deliberately where they don't — not to abandon conventions entirely.

Strictness vs leniency in input validation. Being strict about invalid inputs (return 400 for unrecognised fields) catches consumer bugs early. Being lenient (ignore unrecognised fields) makes forward compatibility easier — consumers on older client versions don't break when new required fields are added server-side. The standard recommendation: be strict in what you send (outputs), lenient in what you accept (inputs).

Depth of response vs multiple round trips. Returning a rich, nested response reduces round trips but couples consumers to a specific data shape. Returning minimal data and requiring consumers to fetch related resources increases round trips but gives consumers more flexibility. GraphQL was invented specifically to solve this tradeoff — let consumers specify exactly what they need. For REST APIs, a practical middle ground is sparse fieldsets (?fields=id,destination,created_at) that let consumers opt into richer responses.


The one thing to remember

Design your API for the consumer who will call it at 2am debugging a production incident — not for the engineer who will implement it. They should be able to read the error message and know exactly what went wrong, look at the URL and know what resource it represents, and trust that the same conventions apply across every endpoint. Every minute you invest in clear naming, consistent structure, and honest errors saves hours of consumer debugging time multiplied across every integration that ever uses your API.


← Previous: APIs & Communication — Overview — what this pillar covers and why it matters

→ Next: REST APIs — API design gives you the principles; REST gives you the architectural style that most HTTP APIs implement. The next post covers REST's constraints, what most APIs get wrong about REST, and how those constraints create real technical benefits.

Systems Design

Part 32 of 50

Understanding these system design concepts is essential for architects, developers, and engineers to create scalable, reliable, and maintainable software systems that meet the needs of businesses.

Up next

REST APIs: Constraints That Create Benefits

Series: System Design · APIs & Communication — Pillar 3 of 8 This pillar: 00 — Overview · 01 — API Design · 02 — REST APIs · 03 — Authentication vs Authorisation · 04 — Session vs Token Auth · 05 — OA