camelCase vs snake_case — API Boundary Naming

webdev api naming case-converter tools

Frontend ships userId. Django returns user_id. Someone hand-maps half the fields and leaves createdAt next to updated_at in the same object. TypeScript compiles. Runtime is undefined. Naming case is a boundary problem, not a taste war — and mixed JSON is how silent production bugs ship.

Conventions by layer

LayerCommon conventionNotes
JSON for JS/TS clientscamelCaseMatches idiomatic JS
Python / Ruby backendssnake_caseMatches language style
HTTP headerskebab-caseContent-Type
URL pathskebab-case/user-profiles/123
SQL columnssnake_caseQuoting hell otherwise
Env varsSCREAMING_SNAKEDATABASE_URL
Protobuf / some gRPCsnake_case JSON or customFollow the IDL option

Pick one for public JSON and document it in OpenAPI. Internal DB names can differ — that is what mappers are for. Bulk-rename field lists with a case converter during migrations, then commit the result so humans review once.

The mixed-payload bug

{
  "userId": "u_1",
  "display_name": "Ada",
  "createdAt": "2026-07-01T00:00:00Z"
}

Clients write user.displayName and get undefined until the next endpoint. GraphQL and tRPC hide some of this; REST JSON over the wire does not. Fix: normalize on serialize/deserialize, not in every screen.

Convert at the boundary (not everywhere)

Good: ORM uses snake_case → serializer emits camelCase → TS types match the wire only.
Bad: Converting in every React component, or storing camelCase in Postgres “because the FE likes it.”

type UserRow = { user_id: string; display_name: string };
type UserDto = { userId: string; displayName: string };

function toDto(row: UserRow): UserDto {
  return { userId: row.user_id, displayName: row.display_name };
}

Libraries (humps, serde rename, Jackson PropertyNamingStrategies) are fine when the team agrees. Magic rename of nested keys surprises you with acronyms (userID vs userId, HTTPResponse vs httpResponse). Prefer consistent lower camel on the wire: userId, httpUrl, oauthToken.

Query params, paths, and headers

Remember the split:

  • JSON bodies follow the JSON policy (camel or snake — pick one).
  • Query params in older APIs may stay ?user_id= even when the body is camelCase.
  • Path segments are usually kebab-case nouns, not camelCase.
  • Headers stay HTTP-style; do not “camelCase” Authorization.

Document both body and query conventions so clients do not “unify” them into one broken style guide.

OpenAPI and generated clients

Generated TypeScript clients amplify whatever OpenAPI says. If the schema uses snake_case properties, your frontend will too — fighting the generator with manual renames recreates the mixed-payload bug. Either:

  1. Define camelCase in OpenAPI and map in the server, or
  2. Accept snake_case in TS and stop rewriting in UI code.

Do not do both half-way. CI can lint example payloads for mixed keys.

Acronyms and pluralization landmines

Wire keyAvoid
userIduserID, UserId
htmlUrlHTMLUrl, HtmlURL
skuIdsSKUIds, skuIDs inconsistently

Plural field names should match product language (items vs itemList) once and forever. Case conversion tools will not fix semantic naming debt.

Fixtures as the contract

Store canonical wire JSON in fixtures; assert serializers and parsers against the same file. Migrating case? Prefer a versioned endpoint (/v2/...) with a sunset date over forever dual-write aliases that never die.

tests/fixtures/user.json  →  golden wire shape
serializer unit test      →  row → dto matches fixture
client contract test      →  fixture → typed object

Migration checklist

  • Inventory public JSON keys (and note outliers)
  • Fail CI on mixed-case keys in fixtures
  • Serializer tests for nested objects and arrays
  • Update FE types in the same PR as API changes
  • Announce breaking renames with a date
  • Update Postman/Insomnia collections and SDK READMEs

Greenfield + TS clients → camelCase JSON is usually least friction. Python monolith with existing snake_case clients → keep snake_case; convert in the SPA at the HTTP client. Naming debates never end; production bugs end when the boundary is boring, tested, and automatic.

Ship the convention in the first OpenAPI example response, not in a wiki nobody reads. New endpoints copy whatever the last engineer pasted — make the happy-path example the standard.

OpenAPI as law

Generate TypeScript clients from OpenAPI so wire names cannot drift in reviews. If a field must rename, bump the API version deliberately and publish a changelog entry.