camelCase vs snake_case — API Boundary Naming
Python returns user_id. TypeScript expects userId. Someone maps fields by hand in three repositories. Six months later half the payloads mix both styles in one object. Naming conventions are cheap; boundary discipline is the actual work.
One convention per layer
| Layer | Common convention | Why |
|---|---|---|
| JSON for JS/TS apps | camelCase | Matches language identifiers |
| Python / DB columns | snake_case | Language and SQL norms |
| HTTP paths | kebab-case | Readable URLs: /api/user-settings |
| Env vars | SCREAMING_SNAKE | Shell tradition |
| CSS | kebab-case | Selectors |
Do not “convert everywhere.” Convert once at the trust boundary — API gateway, BFF, or generated client SDK. Spreading ad-hoc toCamelCase helpers through controllers guarantees drift.
Wire format versus internal format
Good pattern:
DB: user_id
Backend models: user_id (Python)
JSON response: userId ← serializer alias
TS client: userId
Bad pattern:
JSON: { "user_id": 1, "userName": "Ada", "UserID": 1 }
Mixed keys destroy autocomplete, OpenAPI generation, and grep-driven refactors. Reviewers should treat mixed case in one object as a defect, not a style preference.
Mapping without tribal knowledge
- OpenAPI / protobuf / JSON Schema — generate clients; field names stay aligned
- Pydantic / serde / Jackson aliases — declare
user_id↔userIdonce - Manual mappers — only for legacy; isolate in one module named
wire.tsorserializers.py
When migrating a field list, use a case converter to rename bulk identifiers in docs and fixtures — then fix the serializer, not fifty call sites by eye.
kebab-case pitfalls in JSON
{ "user-name": "Ada" }
Valid JSON, miserable in JS: obj.user-name is subtraction. You are stuck with obj["user-name"]. Keep kebab for URLs and CSS:
GET /v1/user-profiles/42
GraphQL usually prefers camelCase field names; REST path segments stay kebab. Do not force one case across HTTP path, JSON body, and SQL column — that is how teams invent a fourth convention.
Acronyms, plurals, and boolean names
Decide once and lint it:
userIdvsuserID(pick JS-idiomaticuserIdunless you are in a Go shop with a writtenIDrule)urlsnotuRLsoauthTokenwith a documented acronym listisActive/hasChildrenfor booleans soactive: falsedoes not read like a noun pile-up
Write the rule in the engineering handbook. Run ESLint naming, Ruff, or equivalent so PR review is not a case court.
Migration playbook that does not strand clients
- Freeze the public wire convention in OpenAPI.
- Add aliases so old and new keys both work during overlap — or version the API (
/v2). - Convert fixtures, mocks, and contract tests in one PR.
- Remove aliases after clients update; fail CI on unknown properties in strict mode.
- Grep for the old key in dashboards and saved queries; analytics often lags app code.
Greenfield defaults that reduce bikesheds
- External JSON:
camelCase - SQL:
snake_case - Paths:
kebab-case - Transform only at the edge
- Acronym policy written before the first public endpoint
Public SDKs should expose the wire names your docs show — do not make every consumer invent mapping. Internal services may speak snake_case to each other if both ends are Python; still document the exception so a future TypeScript worker does not “helpfully” camelize half the fleet.
Naming fights feel stylistic until a payload contains three spellings of the same field. Pick conventions by layer, automate the mapping, and keep converters for migration chores — not for ad-hoc renaming in every feature branch.
Code review cues that catch drift early
Reviewers should flag any PR that introduces a second spelling of an existing field, even temporarily. Temporary becomes permanent in payloads. Prefer failing OpenAPI diff CI when wire names change without a version bump. When integrating a third-party API that disagrees with your convention, isolate adapters in one package so the rest of the codebase stays consistent. A case converter helps migrations and docs; it should not become an excuse to skip deciding the house style. Write the house style in one page: JSON camelCase, SQL snake_case, paths kebab-case, env SCREAMING_SNAKE — then stop debating per ticket.