One Vocabulary: Naming and Consistency
A consumer who has learned one endpoint should be able to predict every other. Casing, id formats, timestamps, money, envelopes, error shapes and header names are decided once, written in a style guide and enforced by a linter — because consistency is the cheapest documentation an API will ever have.
Frame the contract
API design starts with a consumer, a design question and a guarantee — never with a URL.
Inconsistency is a tax on every consumer, paid forever
The same concept spelled three ways — userId in one response, user_id in another, owner in a third; created as epoch seconds here, createdAt as an ISO string there, creation_date as YYYY-MM-DD somewhere else — is not a cosmetic problem. Every consumer writes a mapping layer to normalize it, every SDK generator produces three accessor styles, and every new integrator has to read every endpoint because none can be predicted from the last. The tax is paid on every integration, and it never goes away because fixing it later is a breaking change (Backward Compatibility: The Real Rules).
It arrives without anyone deciding. Team A ships the first endpoints in camelCase because their framework defaults to it; Team B copies a snake_case example from a partner API; a contractor adds epoch timestamps because the database stored them that way. No decision was wrong in isolation; the absence of a shared decision produced the mess. Consistency is therefore an organizational artifact — a written vocabulary and a mechanism that enforces it — not a matter of individual care (API Ownership and the Catalog decides who holds the pen).
The payoff of consistency is that the API becomes learnable by induction. A consumer who knows that lists return { data, next_cursor }, errors return { code, message, request_id, errors[] }, ids look like type_xxxx, and timestamps are RFC 3339 UTC, can write correct code against an endpoint they have never read. That is the sense in which consistency is documentation: it lets the docs say less, and it makes the Documentation Is Part of the Contract that remains describe *what* each operation does rather than re-explaining *how* every field is spelled.
| Decision | Default worth adopting | Why | What inconsistency costs |
|---|---|---|---|
| Field casing | One of snake_case or camelCase, everywhere, including query params | Predictability; SDK generators map one rule | Two accessor styles in every SDK; mapping layers in every client |
| Resource ids | Opaque strings with a type prefix: usr_7f9c | Self-describing in logs; no enumeration; not tied to a table | Integer ids that leak, collide across shards and cannot move |
| Timestamps | RFC 3339 in UTC with Z, suffixed _at | Unambiguous; sorts lexically; every language parses it | Epoch-vs-ISO bugs, off-by-timezone incidents |
| Money | { "amount": 1999, "currency": "EUR" } in minor units | No floats; currency travels with the number | Rounding drift; a 19.99 that becomes 19.989999 |
| Booleans and enums | is_/has_ prefixes; lowercase snake_case enum values | Readable; stable across languages | Mixed active/isActive/ACTIVE in one payload |
| Collections | Plural nouns; envelope { data, next_cursor } | Room for pagination and metadata | Bare arrays that can never grow a cursor |
| Errors | One shape from The Error Model: Structure Over Apology on every operation | Clients branch on code once | Per-endpoint error formats; string matching |
| Headers | Idempotency-Key, Request-Id, Retry-After, RateLimit-* — the same names everywhere | Middleware and SDKs handle them once | Three spellings of the request id header |
| Nulls and absence | Fields always present; null for no value; absent means unchanged on PATCH | Deterministic shape (Request Contracts: Required, Optional, Null and Absent) | Clients checking three conditions per field |
The style guide and the linter
A style guide is the vocabulary written down: one page, versioned, owned, with the decisions above and the reasoning behind each. The reasoning matters more than the choice — snake_case vs camelCase is a coin flip, but "because our SDK generator and our largest consumer use it" survives the next reorganization. The guide also records the exceptions and why they exist (legacy /v1/reports uses epoch timestamps; migrating in Q4), which stops the exception from being copied as a pattern.
A guide nobody enforces is a wish. Enforcement is mechanical: a linter over the OpenAPI: Describing the Contract, Not Designing It description (or whatever schema the API is described in) that fails the build on a non-conforming field name, a missing envelope, a timestamp typed as integer, an error response that is not the shared shape. Spectral-style rulesets exist for this; a custom script over the schema is a day's work. The linter runs in CI on every schema change and in the API review, and its rules *are* the style guide in executable form — this is one of the concrete arguments for Schema-First vs Code-First.
The review process closes the loop. Every new endpoint is reviewed against the guide by someone outside the authoring team; the review asks the induction question — "could a consumer of our other endpoints guess this one?" — and treats a "no" as a defect. The Review Lab's linter findings ("inconsistent id format", "list without envelope") are the same checks, run against a design before it ships.
1GET /users/422{ "userId": 42, "createdAt": 1724580000, "active": true, "Balance": 19.99 }3 4GET /projects/185{ "id": "18", "owner_id": 42, "creation_date": "2026-08-25", "is_active": "yes",6 "members": [ … ] } # bare array, no pagination possible7 8GET /invoices9[ { "InvoiceID": "INV-0001", "created": "25/08/2026 10:14", "amountCents": 1999 } ]10 11# Error from /users: { "error": "not found" }12# Error from /projects: { "message": "Project not found", "status": 404 }13# Error from /invoices: HTML.1GET /users/usr_7f9c2{ "id": "usr_7f9c", "created_at": "2026-08-25T09:20:00Z", "is_active": true,3 "balance": { "amount": 1999, "currency": "EUR" } }4 5GET /projects/proj_18a26{ "id": "proj_18a2", "owner": { "id": "usr_7f9c", "display_name": "A. Bee" },7 "created_at": "2026-08-25T10:14:03Z", "is_active": true }8 9GET /projects/proj_18a2/members?limit=2010{ "data": [ … ], "next_cursor": "eyJ…" }11 12# Every error, every endpoint:13{ "code": "NOT_FOUND", "message": "Project not found", "request_id": "req_…" }The second API needs a third of the documentation because the shape of everything is implied by the shape of anything. The first API needs a mapping layer in every consumer, and an SDK generator produces three different clients for it.
Consistency across time, not just across endpoints
The hardest consistency is temporal. The style guide exists so that the endpoint added in year three matches the one shipped in year one, when the original team is gone. That is why the guide, the linter and the review are a system: the guide holds the decisions, the linter enforces the checkable ones, and the review catches the rest. Any one of them alone decays.
When the vocabulary must change — an id format that has to grow, a timestamp precision that has to increase — the change follows API Migration: Running the Change End to End: both forms accepted on input for a window, one form emitted, telemetry on who still sends the old form, then removal. A vocabulary change is by definition API-wide, which makes it the most expensive kind of breaking change and the strongest reason to get the vocabulary right before the second endpoint ships.
Consistency is not uniformity of *semantics*. Two operations that genuinely differ should look different — a command-style POST /orders/{id}/ship next to resource-style CRUD is fine when Resource or Action? justified it, and The "REST Purity" Anti-Pattern warns against flattening real differences for the sake of visual sameness. The rule is that *the same thing* is spelled the same way; different things are allowed, and encouraged, to look different.
- Write the vocabulary down with the reasoning, one owner, versioned; record exceptions and their expiry.
- Lint the schema in CI: casing, id format, timestamp type, envelope, error shape, header names.
- Review by induction: could a consumer of the other endpoints guess this one?
- Change the vocabulary as a migration, never as a patch; it is API-wide by definition.
- Same things spelled the same; genuinely different operations may look different.
Key points
- Inconsistency is a tax every consumer pays on every integration, forever, because fixing it later is a breaking change.
- It arrives without a decision; consistency is an organizational artifact — a written vocabulary plus mechanical enforcement.
- A consistent API is learnable by induction, which is why it needs less documentation and produces cleaner generated SDKs.
- Style guide, schema linter in CI, and cross-team review are one system; each alone decays over time.
- Consistency means the same thing spelled the same way — not flattening genuinely different operations into one look.
Follow the failure
How the contract fails or gets misused, hop by hop — and what it costs when it completes.
- 1Team A → v1: ships camelCase, integer ids, epoch timestamps — the framework defaults.
- 2Team B → v1: adds endpoints in snake_case with ISO dates, copying a partner API they admire.
- 3Consumer → SDK: the generated client has
userIdanduser_idaccessors on adjacent types; the integrator writes a normalization layer. - 4Team C → year three: copies the nearest example, which is whichever one they opened; inconsistency now has three dialects.
- 5Platform team → cleanup: discovers that unifying the vocabulary is a breaking change across 40 endpoints and 200 consumers.
- Every consumer builds and maintains a mapping layer; SDK generators emit inconsistent clients; onboarding time grows with every dialect.
- Timestamp and money inconsistencies produce real bugs — timezone drift, rounding errors — not just annoyance.
- The API cannot be fixed cheaply: vocabulary unification is the widest possible breaking change.
Design, observe, evolve
A contract decision is incomplete until you know how you would notice it failing and how it changes later.
- • Adopt a style guide before the second endpoint: casing, ids, timestamps, money, enums, envelopes, errors, headers, null semantics — with reasoning.
- • Enforce it with a schema linter in CI and in the API review; fail the build on non-conformance.
- • Prefix ids by type, use RFC 3339 UTC timestamps suffixed `_at`, minor-unit money with currency, and the shared [[error-model]] on every operation.
- • Record exceptions with an expiry, and review new endpoints by asking whether they could be guessed from existing ones.
- • Treat any vocabulary change as an API-wide migration with dual acceptance, telemetry and a deadline.
- • Linter findings per pull request trend toward zero when the guide is alive; a rising count means new teams have not adopted it.
- • Support and onboarding questions of the form "why is this field named differently here?" are the consumer-side cost made visible.
- • Field-name and format variety extracted from the schema (how many timestamp formats, how many id styles) is a measurable consistency score.
- • New fields and endpoints that follow the guide are additive and predictable; the guide is what makes additive growth stay learnable.
- • Vocabulary changes are migrations: accept both forms, emit the new one, measure, deprecate, remove ([[api-migration]]).
- • Recording the reasoning lets a future team revise a decision deliberately instead of re-litigating it per endpoint.
- • A guide and a linter are upfront process cost that feels bureaucratic at endpoint number two and indispensable at endpoint number forty.
- • Strict enforcement occasionally blocks a locally better name; the consistency win usually outweighs it, and the exception mechanism exists for when it does not.
- • Some conventions (prefixed ids, envelopes) add bytes and ceremony to tiny responses.