apis · level 9

API Design Review

What to actually look for in an API PR.

200 XP

API Design Review

This lesson is a checklist. Every PR that touches the API surface should be evaluated against the same rubric, in roughly this order. Most reviews catch one or two of these; few catch all of them. The good ones do.

The 9-point rubric

1. Naming        — resources, paths, casing
2. Status codes  — semantic, not "200 for everything"
3. Error shapes  — single envelope, not three flavours
4. Idempotency   — POST that mutates needs an Idempotency-Key story
5. Pagination    — every list endpoint, cursor or offset
6. Versioning    — what's the breaking-change policy
7. Auth          — explicit scope/role check, not implicit
8. Observability — counter + histogram + span on every handler
9. Consistency   — does this look like the rest of the API

Walk through each one for any PR that adds or changes a public-ish endpoint.

1. Naming

REST resources are nouns, not actions:

  • GET /users/42 — yes
  • POST /api/getUserById — no
  • DELETE /sessions/abc — yes
  • POST /api/forceLogout — almost always wrong

If a verb really doesn't fit a resource (/exports, /searches, /refunds), POST to a verb-as-resource — but stop and check whether the verb IS a resource first.

Casing: pick one and lint for it. snake_case for JSON keys is the most common (Stripe, GitHub). camelCase is fine if your stack is JavaScript-heavy. Never mix. A response that has both userId and created_at in the same object will get bug reports forever.

Path segments: lowercase, kebab-case if multi-word. /users/sessions not /Users/Sessions. URL paths are case-sensitive in the spec but case-insensitive in many proxies, which is a delight to debug.

2. Status codes

The single sin is returning 200 with an error body. Every monitoring tool, every retry middleware, every CDN, every alerting rule treats 2xx as "all good". Burying an error inside breaks them all.

Quick decision table:

Outcome Code
Read OK 200
Created OK 201 (with Location header)
Accepted but async 202
Done, no body 204
Body unparseable 400
No/bad credentials 401
Authenticated but forbidden 403
Resource missing 404
Conflict, unique key 409
Body parsed, fails validation 422
Rate-limited 429 (+ Retry-After)
Unhandled exception 500
Upstream broken 502
Service down (transient) 503 (+ Retry-After)
Upstream timed out 504

Reject 200-with-errors loudly in review.

3. Error shapes

Reviewer asks: "What does an error response look like? Is it the same shape as every other error response in this API?"

If you've already adopted RFC 7807 / 9457 (application/problem+json) — confirm the new endpoint uses the same envelope. If you've adopted a {error: {code, message}} shape — same. The wrong answer is "this endpoint invents a new error format because it had a unique need". Almost always the unique need can be expressed as an extension field on the existing envelope.

Per-field validation errors should be plural, structured, and machine-parseable:

{
  "type": "https://api.example.com/errors/validation-failed",
  "errors": [
    { "field": "email", "rule": "format" },
    { "field": "age",   "rule": "min", "min": 18 }
  ]
}

Not "Validation failed: email is invalid, age must be at least 18".

4. Idempotency

For every POST/PATCH that mutates state, ask: "What happens if a client retries this?"

If the answer is "creates a duplicate" or "applies twice", the endpoint needs an Idempotency-Key story. The reviewer checks:

  • The endpoint accepts (or requires) Idempotency-Key.
  • The body fingerprint is verified against any cached entry.
  • The cache TTL is documented (24h is conventional).
  • A mismatched fingerprint returns 422, not silent re-execution.

PUT and DELETE are idempotent by spec — they don't need the header. POST and non-idempotent PATCH do.

5. Pagination

Every list endpoint must paginate. The reviewer asks: "What happens when this returns 100,000 rows?"

Two acceptable answers:

  • Cursor pagination: each response includes a next_cursor; the client passes it back. Stable under concurrent writes, but doesn't support "jump to page N".
  • Offset pagination: the client sends offset and limit. Supports random access, but causes skips/duplicates if rows shift between page fetches.

Pick one per endpoint. Document the default page size and the maximum page size. Reject "the endpoint returns everything" without comment.

6. Versioning

The reviewer asks: "Is this a breaking change? If so, how is it being rolled out?"

Definitely breaking:

  • Removing a field from a response.
  • Renaming a field.
  • Narrowing the type (string → enum).
  • Adding a required field to a request.
  • Changing the meaning of an existing field.

Definitely safe:

  • Adding a new optional field to a request.
  • Adding a new field to a response (clients should ignore unknowns).
  • Adding new endpoints.
  • Adding new enum values to a response (clients should ignore unknowns; document this expectation).

Borderline (verify with consumers):

  • Adding new enum values to a request — fine if optional.
  • Loosening a type (enum → string) — usually OK but check.

When breaking, the rollout options are:

  • Bump the API version (path: /v2/..., header: X-API-Version: 2, query: ?version=2). Maintain v1 with a documented deprecation date.
  • Add the new field, deprecate the old one, sunset the old one after a generous window (90 days for B2B, sometimes years for B2C).

The wrong answer is "breaking change shipped without a version bump because it's only one field".

7. Auth

Every endpoint must declare its auth posture:

  • Public — no auth required (and explicitly so).
  • User-bearer — requires a user session token; the handler checks the user.
  • Scoped token — requires an OAuth token with specific scopes.
  • Role-gated — requires a specific role (admin, billing, etc).

The reviewer checks:

  • The auth check is explicit at the top of the handler, not buried 5 levels deep.
  • The check is not relying on framework middleware that silently 401s if you forget to opt in — you want a fail-closed default that errors loudly when an endpoint is added without an auth declaration.
  • The error returned for "wrong auth" is the right code — 401 if reauth would help, 403 if it wouldn't.

8. Observability

Every handler should emit:

  • A counter of requests, tagged by status class (api.requests.total{status="2xx"}).
  • A histogram of latency.
  • A tracing span (named for the operation, with tags for the resource ID).
  • A log line on errors including a request ID.

Most frameworks make this trivial via middleware. The reviewer checks the new endpoint actually inherits the middleware (and that the handler doesn't bypass it for some reason).

9. Consistency

Last, the gestalt question: "Does this look like the rest of the API?"

  • Does it use the same auth scheme?
  • Does it return errors in the same envelope?
  • Does it use the same casing conventions?
  • Does it use the same pagination style?
  • Does it use the same versioning scheme?

If the new endpoint stands out — different envelope, different casing, different auth — the reviewer asks: "Why? If we accept this, every future endpoint either matches the old style or matches the new style. Pick one."

This is the hardest review point because it's subjective. The fix is a documented style guide (Microsoft, Google AIP, Stripe — pick one and link to it from your CONTRIBUTING.md) and a linter (Spectral for OpenAPI, Buf for Protobuf) that enforces it in CI.

Anti-patterns to recognise

  • "It's just an internal endpoint" — internal endpoints leak. Hold the same bar.
  • "We can fix it later" — APIs are forever. Customers integrate, then it's harder to fix.
  • "The schema doesn't matter, the docs explain it" — the schema IS the docs. If the schema is wrong, the docs lie.
  • "Tests pass" — schema tests don't catch design problems. They catch correctness.

How to disagree

When you find a problem, propose the fix. Not "this is bad" — "this is bad, here's what good looks like":

Suggest renaming getUserPermissionsGET /users/{id}/permissions. The current name is RPC-style and inconsistent with the rest of the API; the rename matches existing endpoints like GET /users/{id}/sessions.

The author should walk away knowing exactly what to change. The reviewer should be willing to say "you're right, I'll do that" or "here's why I went with the original" without ego on either side.

TL;DR

Walk the rubric. Lint the schema. Consult the style guide. Ship consistency.

Tools in the wild

4 tools
  • Spectralfree tier

    OpenAPI/AsyncAPI linter — pre-built rule packs for the major style guides.

    cli
  • Buffree tier

    Protobuf linter + breaking-change detector for gRPC schemas.

    service
  • Opticfree tier

    Detects accidental API breaking changes by diffing OpenAPI between commits.

    service
  • API Stylebookfree tier

    Curated list of public API style guides — consult before writing your own.

    spec