The API is the one part of your system you can never take back. Once someone integrates against it, it’s a promise — and breaking a promise breaks their code.
Prerequisites: HTTP & TLS, Client–Server Model Time to read: ~18 minutes
Every other part of your system is yours to change. You can swap the database, refactor a service, rewrite the algorithm — as long as behaviour is preserved, nobody notices.
🚨 The API is different, because other people’s code depends on its exact shape. A mobile app shipped last year, a partner’s integration, a script someone wrote — all encode assumptions about your API. Change it and their code breaks, and you can’t fix their code.
This asymmetry is the whole reason API design matters. An internal function you can rename in seconds; a public field you can be stuck supporting for a decade. Design accordingly.
🚨 The most common mistake: exposing your internal data model as the API.
// ❌ Your database tables, leaked
{"usr_id": 42, "usr_fname": "B", "usr_lname": "A", "addr_tbl_fk": 991, "created_ts": 1735689600}
// ✅ What the consumer actually wants
{"id": 42, "firstName": "Bilal", "lastName": "Aslam",
"address": {"city": "Lahore"}, "createdAt": "2026-07-22T10:00:00Z"}
The API is a contract designed for its users, not a window onto your schema. Coupling them means you can never refactor your database without breaking clients, and clients inherit your table naming, your foreign keys, and your internal concepts.
Think in terms of the use cases the consumer has, and shape the API around those.
🚨 The single most important quality of a good API is that it’s predictable. If a developer can guess the next endpoint from the pattern of the last one, you’ve succeeded.
Pick conventions and apply them everywhere:
camelCase or snake_case — pick one, never mix. (JSON APIs usually camelCase;
URLs usually lowercase with hyphens.)/users, /orders — collections are plural, always.2026-07-22T10:00:00Z) — always. Never epoch here, never a local
timezone.An API where /users returns user_name and /orders returns orderName and /products returns
ProductTitle forces the developer to check the docs for every single field. Consistency is a
feature.
The API should behave the way a reasonable developer expects. A GET never has side effects. A
DELETE on something already deleted returns success (or 404), not an error. A list endpoint returns
an empty array [], not null, when there’s nothing. 201 Created returns the created resource.
🚨 Surprises are bugs waiting to happen in your consumers’ code — and you’ll never see them until they file a support ticket.
Postel’s Law, and it has a real subtlety:
🚨 The critical corollary: clients must ignore unknown fields in responses. This is what lets you add fields to your API without breaking existing clients — the single most valuable piece of forward-compatibility. Document it and enforce it in your own client libraries. → Versioning
The 90% use case should require minimal effort — sensible defaults, few required parameters. The rare advanced case should be possible without cluttering the common path.
GET /orders → sensible defaults (recent, paginated, common fields)
GET /orders?status=shipped&fields=id,total&sort=-createdAt&expand=customer → power user
200 with {"success": false}.
→ Error HandlingamountCents, durationMs) — 🚨 a field called amount or timeout with an
ambiguous unit is a classic source of bugs."status": "shipped") over magic numbers ("status": 3).Make reads safe and repeatable operations idempotent, and give clients a way to make writes idempotent (an idempotency key on POST). This isn’t optional polish — it’s what makes your API usable over an unreliable network, where clients must retry. → Idempotency ⭐
🚨 Never return an unbounded list. GET /users on a table with 10 million rows is a design error
that will take down your database and the client. Every collection endpoint needs a default limit and
pagination — retrofitting it is a breaking change.
→ Pagination
The URL structure is the most visible part of your API. Conventions:
GET /users list users
POST /users create a user
GET /users/42 get one user
PUT /users/42 replace user 42
PATCH /users/42 partially update user 42
DELETE /users/42 delete user 42
GET /users/42/orders list user 42's orders (nested resource)
GET /orders/991 get order 991 directly (also accessible at top level)
Rules:
/users, not /getUsers or /createUser. The HTTP method is the verb./users/42/orders/991/items/5 is too much — expose
/orders/991 and /items/5 at the top level too. Two levels of nesting is a reasonable ceiling./shipping-addresses), never underscores or
camelCase in URLs.🚨 The actions-that-aren’t-resources problem. How do you model “cancel an order” or “send an
email”? Options: a sub-resource state change (PATCH /orders/991 {"status": "cancelled"}), a
sub-collection (POST /orders/991/cancellation), or a pragmatic action endpoint
(POST /orders/991/cancel). Purists dislike the last one; it’s widely used and perfectly clear. Be
pragmatic — a readable action endpoint beats a contrived resource.
This is a per-context decision, and each has its own chapter:
| Style | Best for | Chapter |
|---|---|---|
| REST | Public APIs, CRUD, broad compatibility | REST |
| GraphQL | Flexible client-driven data fetching, many clients | GraphQL |
| gRPC | Internal service-to-service, high performance | gRPC |
| Webhooks | Server-to-client notifications of events | Webhooks |
🎙️ The default recommendation: “REST/JSON at the public edge for compatibility and debuggability, gRPC internally for performance, GraphQL where clients need flexible data shapes, and webhooks for event notifications to third parties.” Most large systems use all four for different purposes.
🚨 An undocumented API might as well not exist. The documentation is the product, for an API.
Machine-readable specs are the foundation:
.proto file is the contract.Why a machine-readable contract matters beyond docs: it lets you generate clients (so consumers don’t hand-write them), validate requests and responses automatically, catch breaking changes in CI, and run consumer-driven contract tests — where consumers declare what they depend on and the provider verifies it before deploying. That last one is how you safely evolve an API with consumers you can’t coordinate with.
| Principle | Gain | Cost |
|---|---|---|
| Consumer-shaped API | Clients get what they need; you can refactor internally | A translation layer between API and storage |
| Strict consistency | Predictable, guessable | Discipline; harder to make exceptions |
| Accept-liberal/send-strict | Forward compatibility | Clients must ignore unknown fields |
| Explicit versioning | Safe evolution | More surface to maintain |
| Pagination everywhere | Bounded responses; scalable | Slightly more complex endpoints |
| Rich query support | Powerful for consumers | More to implement, test, and secure |
usr_id,
created_ts, foreign keys exposed — and it’s the thing teams most regret, because it couples the
public contract to internal storage permanently./getUser) instead of nouns plus HTTP methods.200 with an error in the body. → Error Handlingamount, timeout) without Cents/Ms suffixes.GET /users is a design error, and adding pagination later is a breaking change.”1. Redesign a leaked API. Find a real API (or one you’ve built) that exposes internal names —
user_id, created_at, foreign keys. Redesign it as a consumer-shaped contract. Notice how the
translation layer decouples the two.
2. Write an OpenAPI spec. Take three endpoints and write their OpenAPI definition. Then generate a client and server stub from it. Seeing a working client generated from a spec makes the value of a machine-readable contract concrete, and it takes twenty minutes.
3. Audit for consistency. Take any multi-endpoint API and check: are all collections plural? Are all dates the same format? Is the error shape identical everywhere? Are naming conventions uniform? Most APIs fail at least one, and each inconsistency is friction for every consumer.
4. Model the tricky cases. How would you design: cancel an order, resend a receipt, bulk-import users, search across resources, and a long-running export? Each pushes on “everything is a noun/CRUD” and forces pragmatic decisions.