camelCase vs snake_case — API Boundary Naming

(Updated: July 16, 2026 ) webdev naming api case-converter tools

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

LayerCommon conventionWhy
JSON for JS/TS appscamelCaseMatches language identifiers
Python / DB columnssnake_caseLanguage and SQL norms
HTTP pathskebab-caseReadable URLs: /api/user-settings
Env varsSCREAMING_SNAKEShell tradition
CSSkebab-caseSelectors

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_iduserId once
  • Manual mappers — only for legacy; isolate in one module named wire.ts or serializers.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:

  • userId vs userID (pick JS-idiomatic userId unless you are in a Go shop with a written ID rule)
  • urls not uRLs
  • oauthToken with a documented acronym list
  • isActive / hasChildren for booleans so active: false does 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

  1. Freeze the public wire convention in OpenAPI.
  2. Add aliases so old and new keys both work during overlap — or version the API (/v2).
  3. Convert fixtures, mocks, and contract tests in one PR.
  4. Remove aliases after clients update; fail CI on unknown properties in strict mode.
  5. 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.