Principles for building APIs that are intuitive, stable, and easy to consume.
If you are new here: An API (Application Programming Interface) is how one program asks another for data or work over the network. Your mobile app or website is usually the client; the server (or service) answers with HTTP responses. The contract is everything both sides rely on: paths, JSON shapes, status codes, and error formats. API design is the discipline of making that contract clear and stable so updates do not accidentally break every installed app.
| Term | Plain meaning |
|---|---|
| Client | Anything calling your API |
| Server / service | The program handling the request |
| Contract | Documented rules clients and servers share |
| Breaking change | A change that requires client updates (rename/remove field, change type) |
It's 2 AM. Crash reports spike because someone renamed a JSON field from user_id to userId. The server is healthy, but old app builds still expect user_id. That is a contract failure: the server changed the rules without a version or migration path clients could follow.
In plain terms: API design is how you keep clients and servers agreeing on paths, payloads, errors, and evolution — so shipping does not silently brick apps in the wild.
That is the problem this lesson exists to solve: predictable contracts instead of “it works if everyone deploys at once.”
| Symptom | What usually happened |
|---|---|
| Clients crash on parse | Field removed or renamed without a new API version |
| Chaos in retries | 200 responses that secretly contain errors |
| Slow incidents | Errors are only human sentences—no code or requestId |
Everything below is how to avoid that class of failure.
Model your world as resources—things you can name—not as RPC-style URLs that encode the action in the path.
| RPC-style (harder to reason about) | Resource-style (common REST pattern) |
|---|---|
GET /getUser?id=1 | GET /users/1 |
POST /createOrder | POST /orders |
GET /fetchAllProducts | GET /products |
| Path | Meaning |
|---|---|
/users | Collection—list users, or create a new user (with POST) |
/users/42 | Single item—one user by id |
Predictable URLs help caching, routing, logs, and onboarding: people guess right before reading your code.
Example — same user, two operations:
GET /users/42 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbG…HTTP/1.1 200 OK
Content-Type: application/json
{"id":"42","display_name":"Asha","email":"[email protected]"}Creating a user hits the collection; the server chooses the new id:
POST /users HTTP/1.1
Host: api.example.com
Content-Type: application/json
{"display_name":"Ben","email":"[email protected]"}HTTP/1.1 201 Created
Location: /users/43
Content-Type: application/json
{"id":"43","display_name":"Ben","email":"[email protected]"}HTTP methods (verbs) tell intermediaries and clients what kind of operation this is. The ecosystem assumes:
| Method | Typical use | Important habit |
|---|---|---|
| GET | Read data | Should be safe (no lasting side effects) |
| POST | Create or start a process | Repeats may duplicate—use idempotency keys for payments |
| PUT | Replace a resource fully | Often treated as idempotent |
| PATCH | Partial update | Document exactly what “partial” means |
| DELETE | Remove | Often idempotent at the HTTP level |
If GET deletes data or POST only reads, caches and retry logic will lie to you—bugs show up under load, not in demos.
The status code is the first thing automated clients use. Use it honestly.
| Range | Meaning |
|---|---|
| 2xx | Success |
| 3xx | Redirect—client should follow rules in headers |
| 4xx | Caller problem—fix request, auth, or permissions |
| 5xx | Server problem—retry with backoff may help |
| Code | Use it when |
|---|---|
| 200 / 201 / 204 | Successful read / create / no body |
| 400 | Validation or malformed input |
| 401 | Not logged in / bad token |
| 403 | Logged in but not allowed |
| 404 | Resource does not exist |
| 409 | Conflict—duplicate, stale version |
| 429 | Rate limited |
| 503 | Temporary overload or maintenance |
Never return 200 with only {"error": "..."} in the body—clients and caches will treat it as success.
Example — wrong vs right:
HTTP/1.1 200 OK
Content-Type: application/json
{"error": "User not found"}Better:
HTTP/1.1 404 Not Found
Content-Type: application/json
{"error":{"code":"USER_NOT_FOUND","message":"No user for that id.","requestId":"req_9c1"}}Structured errors include a stable machine code, a message for humans, optional field for validation, and a requestId that matches your logs and traces.
{
"error": {
"code": "USER_NOT_FOUND",
"message": "No user exists for that id.",
"requestId": "req_8f3a2b"
}
}| Piece | Why it matters |
|---|---|
code | SDKs and apps branch without parsing English |
message | Support and UI copy |
field | Form-level validation feedback |
requestId | One-click correlation in observability tools |
Example — validation with a field pointer:
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Email format is invalid.",
"field": "email",
"requestId": "req_3d7f91"
}
}Breaking changes—removing fields, changing types, renaming keys—must be obvious in your API surface. Silent breaks strand mobile users until they update the app.
| Strategy | Trade-off |
|---|---|
URL /v1/..., /v2/... | Very visible; easy in logs and docs |
Header API-Version | Clean URLs; caches must be configured carefully |
| Package / date versioning | Common for large public platforms |
See the API Versioning lesson for deprecation timelines and additive patterns. Rule of thumb: old clients keep working until they explicitly move.
Example — same resource, two major versions:
| Client built for | Calls |
|---|---|
| Old app | GET https://api.example.com/v1/users/42 → JSON uses user_id |
| New app | GET https://api.example.com/v2/users/42 → JSON uses id |
Or one URL with a header:
GET /users/42 HTTP/1.1
Host: api.example.com
API-Version: 2025-01-01OpenAPI describes endpoints, parameters, bodies, and responses in machine-readable YAML/JSON. Teams use it for docs, code generation, contract tests, and breaking-change detection in CI.
| Artifact | What it enables |
|---|---|
Checked-in openapi.yaml | Reviewable diffs for API changes |
| Generated SDKs | Clients stay aligned with the server |
| Mock servers | Frontend work before backend is done |
The spec is not bureaucracy—it is how multiple teams ship without breaking each other.
Example — tiny OpenAPI fragment (YAML):
paths:
/users/{id}:
get:
summary: Get one user
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/User"
"404":
description: Not foundYour real file grows to list every path your API exposes; CI can diff two versions and flag removed fields.
| Upfront cost | Long-term win |
|---|---|
| Consistent nouns + verbs | Tooling and caching behave correctly |
| Real status codes | Sensible retries and monitoring |
| Structured errors | Faster debugging and better UX |
| Explicit versions | Safer evolution for mobile and enterprise |
| OpenAPI | Single source of truth for the contract |
Design as if you do not control every client. Field renames are cheap in git; they are expensive in production. Next lesson: REST APIs—pagination, caching, and idempotency in practice.
Even with an OpenAPI file checked in, ad-hoc path names still ship when the spec is not enforced in code review — chaos wins.