Foundations · Lesson 02
What makes an API good
If an API is a contract (Lesson 01), then a good API is a contract people enjoy depending on — easy to learn, hard to misuse, and safe to evolve. These qualities aren't taste; they're the exact things interviewers probe when they say "critique this API."
By the end you'll be able to
- List the qualities of a well-designed API and recognise their opposites.
- Explain the design-first vs code-first approaches and when each fits.
- Walk the API design lifecycle from requirements to deprecation.
The qualities of a good API
A useful mental checklist — every interview critique maps onto one of these:
| Quality | Means | Smells bad when… |
|---|---|---|
| Easy to learn | Predictable names, consistent patterns | every endpoint invents its own conventions |
| Hard to misuse | The obvious call is the correct call | you must read three docs to avoid a footgun |
| Consistent | Same idea expressed the same way everywhere | userId here, user_id there, uid elsewhere |
| Minimal but complete | Covers the needs, nothing redundant | five ways to do the same thing |
| Evolvable | Can add features without breaking callers | any change forces every client to update |
| Well-documented | Clear contract, examples, error shapes | "the code is the docs" |
A good API is designed from the outside in — start from "what does the client want to accomplish?" and work back to the implementation. A bad one is designed inside-out, exposing whatever was easy for the server. Recall Lesson 01: leaking internals into the contract is the cardinal sin because it kills evolvability.
Design-first vs code-first
There are two ways to arrive at a contract, and the order matters:
Design-first writes the contract — typically an OpenAPI document — and gets everyone to agree before code exists. Front-end and back-end teams then build in parallel against a stable target, and you can mock the API for testing on day one. Code-first writes the server first and derives the contract from it: quicker for a solo prototype, but the public interface ends up shaped by whatever was convenient internally — exactly the inside-out trap.
Whenever you're asked to "design an API," you're doing design-first by definition — you're writing the contract on the whiteboard before any code. Saying so explicitly ("I'll design the contract first so clients and servers can agree and build in parallel") frames your whole answer and shows you know why the exercise is structured that way.
The API design lifecycle
An API isn't a one-time artifact — it lives, changes, and eventually dies. Keeping the whole arc in mind separates designers from coders:
- Requirements — who calls it, what do they need, what scale? (This is where every case study in this course starts.)
- Design the contract — resources, operations, request/response shapes, errors.
- Review & agree — consumers sanity-check it before code is written.
- Build & document — implement to the spec; docs and examples ship with it.
- Evolve — add capabilities without breaking existing callers (versioning, additive changes).
- Deprecate — sunset old versions gracefully, with notice and migration paths.
Treating "design the contract" as the finish line. The hardest part is step 5 — evolving without breaking callers. Every design choice you make early (did you leak internals? are responses extensible?) decides how much pain step 5 brings. Good APIs are designed so that they can change.
Under the hood: a concrete BEFORE → AFTER endpoint redesign
Saying "design for the caller" is abstract. Here is a real endpoint redesign — starting from what a code-first developer might ship, mapping each problem to a quality from the table above, and showing the corrected version with the reasoning.
BEFORE — the inside-out version:
# What a junior developer might ship after building the DB schema first
POST /api/doUserStuff
Content-Type: application/json
{
"usr_tbl_id": 42, // ❌ DB column name leaked into contract
"action": "deactivate", // ❌ verb in body; HTTP method unused
"ts": 1750000000 // ❌ Unix epoch; not human-readable, not ISO-8601
}
# Response — also broken:
HTTP/1.1 200 OK
{
"res": "ok", // ❌ "res"? what does that mean?
"err": "" // ❌ empty string to mean "no error" — use the status code
}
| Problem | Quality violated | Why it hurts |
|---|---|---|
doUserStuff — verb in path | Consistency / easy to learn | HTTP already has methods (DELETE, PATCH…); duplicating intent in the path breaks every REST client and linter |
usr_tbl_id — DB column name | Evolvability | Every client now depends on your internal schema; rename the column → breaking change |
"action": "deactivate" in body | Hard to misuse | Callers must know the magic string; typos silently succeed; the HTTP method is meaningless |
"ts": 1750000000 Unix epoch | Easy to learn | Not human-readable; timezone ambiguous; RFC 3339 / ISO-8601 is the universal contract |
"res": "ok" and empty "err" | Consistent / documented | Invents a bespoke response envelope instead of using HTTP status codes — callers must parse strings to know if it failed |
AFTER — the redesigned version:
# Resource-oriented, uses HTTP semantics, hides internals
PATCH /v1/users/42
Content-Type: application/json
{
"status": "inactive" // ✓ field name is a concept, not a column
} // ✓ PATCH = partial update; the verb is the HTTP method
# Response:
HTTP/1.1 200 OK // ✓ status code carries success/failure — no body parsing needed
Content-Type: application/json
{
"id": 42,
"status": "inactive",
"updated_at": "2025-06-15T10:30:00Z" // ✓ ISO-8601 with timezone
}
# Error case — returns a machine-readable problem object:
HTTP/1.1 422 Unprocessable Entity
{
"error": "invalid_status",
"message": "status must be 'active' or 'inactive'"
}
The AFTER version satisfies all six qualities: it is easy to learn (standard HTTP semantics), hard to misuse (wrong method returns 405, wrong value returns 422 with a clear message), consistent (every user endpoint follows the same /v1/users/:id + HTTP method pattern), minimal (one field to express intent), evolvable (add fields to the response without breaking callers), and documented (the response shape is self-describing).
How to debug & inspect it — reviewing an API for quality
Reviewing an API for the qualities above is an active skill, not a vibe check. The process below applies equally to a PR review, a design session, or an interview critique question.
Run a linter first. If the API has an OpenAPI spec, Spectral catches the mechanical issues (missing descriptions, undefined response codes, wrong types) before human review. Install and run it against a spec file:
After the linter, walk each endpoint through this structured checklist:
| Check | What to look for | Smell / fix |
|---|---|---|
| Method semantics | Does the HTTP method match what the endpoint does? | GET that mutates data → change to POST/PATCH/DELETE; safe operations on POST → move to GET |
| Path naming | Are paths nouns? Is the resource hierarchy logical? | /getUser, /doThing → /users/:id; verbs in paths → use the HTTP method |
| Status codes | Does every error case return the right code? | 200 for errors, 500 for validation errors → use 400/422 for client errors; never return 200 {"error":"…"} |
| Field naming | Are field names stable concepts, not DB internals? | usr_tbl_id, col_nm → rename to userId, name; pick one case convention (snake_case or camelCase) and apply it everywhere |
| Timestamps | Are all date/time values in RFC 3339 / ISO-8601? | Unix epoch integers → convert to "2025-06-15T10:30:00Z"; include timezone offset always |
| Error shape | Is there a consistent error response object? | Inconsistent shapes → adopt one envelope ({"error": "code", "message": "…"}) across all endpoints |
| Evolvability | Can fields be added without breaking callers? | Clients that break on unknown fields → document that extras should be ignored; avoid changing field types in-place |
Review checklist (use this in a code review or interview):
- Does every path use a noun resource, not a verb? (No
/doThingor/getUser.) - Does the HTTP method match the operation's semantics (safe = GET, idempotent mutation = PUT, partial update = PATCH)?
- Are all status codes semantically correct — 2xx success, 4xx client errors, 5xx server errors?
- Does the response body hide all storage/internal details (no column names, no ORM artefacts)?
- Are all timestamps ISO-8601 with a timezone? Are all IDs strings (not integers that overflow JSON in some languages)?
- Is there a consistent, documented error response shape across every endpoint?
- If there's an OpenAPI spec: does
spectral lintpass clean? Are all operations described?
🧠 Quick check
1. "Hard to misuse" mainly means:
A well-designed API guides callers into the pit of success — the natural call does the right thing, so mistakes are hard to make in the first place.
2. The biggest risk of a code-first approach is that:
Deriving the contract from code tends to leak whatever was convenient internally — the inside-out design that hurts consistency and evolvability.
3. Which lifecycle stage is usually the hardest, and the one good design optimises for?
Anyone can ship v1. The discipline is changing it later without breaking the clients who depend on it — which earlier choices (no leaked internals, extensible responses) make possible.
✍️ Drill: critique a bad endpoint
An interviewer shows you: GET /getUserData?id=42&action=delete. Name three things wrong with it before reading on.
Model answer: (1) Verb in the path — getUserData duplicates what the HTTP method already says; the resource should be /users/42. (2) A destructive action behind GET — action=delete makes a safe, cacheable, retryable method delete data; a crawler or prefetch could wipe users. Use DELETE /users/42. (3) Inconsistent style — mixes a "get" prefix with an "action" param instead of letting method + resource express intent. Bonus: it's not evolvable or self-describing.
Rubric: ✓ spots the method/semantics mismatch (the big one) ✓ notes verb-in-path / RPC-flavoured naming ✓ proposes the resource-oriented fix.
Key takeaways
- Good APIs are easy to learn, hard to misuse, consistent, minimal, evolvable, documented.
- Design outside-in (from the caller's needs), never inside-out from your storage.
- Design-first writes the contract before code; an interview "design this API" is design-first.
- The lifecycle runs requirements → design → agree → build → evolve → deprecate; evolvability is the hard part you design for.
Sources & further reading
- Microsoft — Web API design best practices
- Google — API design guide
- OpenAPI Specification (the design-first contract format)