iKit
Guide · 10 min read ·

REST API Naming: snake_case Backend, camelCase Frontend (2026)

REST API naming forces a choice: snake_case on the backend, camelCase in the client. Here is where to convert, what the round trip loses, and how to decide.

REST API Naming: snake_case Backend, camelCase Frontend (2026)

REST API Naming: snake_case Backend, camelCase Frontend

Your Rails or Django models use snake_case. Your React code uses camelCase. Somewhere between the database and the component tree, created_at has to become createdAt — or not. REST API naming looks like a style argument until a user_uuid turns into userUUID, round-trips back as user_u_u_i_d, and a PATCH silently drops a field. Here is where the conversion belongs.

TL;DR

  • camelCase is the mainstream JSON default; snake_case is fine if applied consistently.
  • Convert once, in the serializer at the service boundary — never in the ORM or components.
  • snake_case → camelCase → snake_case is lossy when an underscore precedes a digit.
  • Acronyms are the other trap: title-case them, never scream them, on both sides.
  • Generated clients from a schema beat runtime regex conversion every time.

Should a REST API use snake_case or camelCase?

JSON itself has no opinion. A JSON object member name is just a string, so {"created_at": ...} and {"createdAt": ...} are equally valid documents. The pressure comes entirely from the languages on either end.

Why does Google's JSON style guide require camelCase?

The Google JSON Style Guide states that property names must be camel-cased ASCII strings, and gives a concrete reason rather than an aesthetic one: the rules mirror JavaScript identifier naming, so clients can reach properties with dot notation — result.thisIsAnInstanceVariable. A key like created_at still works with dot notation in JS, but first-name does not, and once you allow one non-identifier key you have to remember which ones are safe.

Microsoft's Azure REST API Guidelines go further and make it a hard rule: use camel case for all JSON field names, and do not upper-case acronyms — urlValue, not URLValue. They also require JSON field names to be treated as case-sensitive, which is worth stating out loud because a surprising number of hand-rolled clients normalise keys before comparing them.

Does snake_case actually cost you anything?

Not much, and some very large APIs use it. Stripe and Slack both ship snake_case payloads and nobody has struggled to consume them. The real cost is inconsistency, not casing:

  • Mixed casing inside one payload (user_id next to createdAt) means every consumer keeps a mental exception list.
  • Casing that varies by endpoint means you cannot write one generic client-side transform.
  • Casing that changes between API versions breaks every cached type definition.

Pick one. Write it in the style guide. Enforce it in CI with a schema lint rather than in code review.

Where does protobuf land on this?

Protobuf is the clearest illustration that both conventions can coexist if a schema arbitrates. The protobuf style guide mandates lower_snake_case for field names in .proto files, while the ProtoJSON format maps those field names to lowerCamelCase when serialising to JSON. Parsers accept both spellings, and a json_name option lets you pin an exact wire name when the automatic mapping is wrong.

That is the model to copy: snake_case in the schema, camelCase on the wire, with the schema — not a regex — deciding the mapping.

Where to convert between snake_case and camelCase

There are four plausible layers. Only one is right.

Layer What happens Verdict
Database / ORM Columns renamed to camelCase Avoid — SQL folds case
Serializer Schema-aware rename at the edge Correct
HTTP client wrapper Blind deep transform on every response Workable fallback
Components Each file handles its own keys Never

Convert in the serializer, not the ORM

Renaming database columns to satisfy a frontend is the worst version of this. Unquoted identifiers in PostgreSQL fold to lowercase, so a createdAt column becomes createdat unless every query quotes it. You end up with "createdAt" in raw SQL forever, and your analytics team inherits the problem.

Your serializer already knows the full field list. Django REST Framework's source argument, Rails' jbuilder key formatting, and Laravel's API Resources all exist precisely for this. Do it there:

# Django REST Framework
class OrderSerializer(serializers.ModelSerializer):
    createdAt = serializers.DateTimeField(
        source="created_at", read_only=True
    )
    lineItems = LineItemSerializer(
        source="line_items", many=True
    )

    class Meta:
        model = Order
        fields = ["id", "createdAt", "lineItems"]

Verbose, yes — and that verbosity is the point. Every renamed field is visible in one file, greppable, and reviewable. A magic global transform hides the day someone adds tax_rate_2 and it arrives as taxRate2.

When a blind client-side transform is acceptable

If you consume a third-party snake_case API and cannot change it, a deep transform in your HTTP wrapper is fine — one function, one place, applied on the way in and reversed on the way out. The rule is that it lives in the client layer and nothing downstream ever sees the original keys. Paste a sample response into a JSON decoder first to confirm the shape, then write the transform against real keys rather than the documentation's keys.

Do not convert in components

The failure mode here is not aesthetic. When each component decides, you get data.created_at in one file, data.createdAt in another, and an optional-chaining fallback data.createdAt ?? data.created_at in a third. That third pattern is how a field silently reads undefined for six months.

How to convert JSON keys to camelCase in JavaScript

The conversion itself is four lines. The recursion around it is what people get wrong.

const toCamel = (s) =>
  s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());

export function camelizeKeys(input) {
  if (Array.isArray(input)) return input.map(camelizeKeys);
  if (input === null) return input;
  if (typeof input !== "object") return input;
  return Object.fromEntries(
    Object.entries(input).map(([k, v]) => [
      toCamel(k),
      camelizeKeys(v),
    ]),
  );
}

Two things to notice. The regex only matches _ followed by a lowercase letter, so custom_label_0 is left with its underscore rather than being quietly mangled — you will see the odd key instead of losing it. And Date, Map, and File instances are objects, so a transform applied anywhere other than immediately after JSON.parse will flatten them into plain objects. Run it on parsed JSON only.

Can JSON.parse's reviver rename keys?

This is the obvious-looking shortcut, and it does not work. MDN's JSON.parse reference is explicit that the reviver is called with a key and a value, and that its return value redefines the property's value — the property name is not yours to change. Returning undefined deletes the property rather than renaming it, and the reviver walks depth-first from the most nested value outward, so mutating the holder object mid-walk gives order-dependent results.

// Does NOT rename anything — value only.
JSON.parse(body, (key, value) => value);

Rebuild the object instead, as in camelizeKeys above. It is slower on paper and irrelevant in practice: for payloads under a few megabytes the transform costs less than the parse it follows.

Converting the other direction for writes

A PATCH body has to go back. Reuse one snakeizeKeys helper symmetrically — and if you are converting a fixed list of field names rather than live payloads, the case converter does the whole list in one paste, which is faster than writing a throwaway script for a migration checklist.

Why converting camelCase back to snake_case is lossy

This is the part most teams discover in production. The round trip is not an identity function.

Why user_id survives but custom_label_0 does not

The ProtoJSON spec documents the failure precisely, in the context of FieldMask paths that must be converted without a schema to consult. Field names containing an underscore followed by a number do not round-trip: x_0 camelises to x0, and converting x0 back to snake_case leaves x0. The underscore is gone, and no rule can recover it. The same applies to leading, trailing, and doubled underscores — _x, foo__bar, __foo_bar are indistinguishable from ordinary names after the trip.

Original camelCase Back to snake_case
user_id userId user_id
ipv6_address ipv6Address ipv6_address
custom_label_0 customLabel0 custom_label0
foo__bar fooBar foo_bar

Protobuf's answer is to forbid the ambiguous shapes at the source: the style guide says an underscore must always be followed by a letter, never a digit or a second underscore, and starting with Edition 2024 protoc rejects those names outright. Adopt the same rule for your own field names and the lossiness never fires.

The acronym trap

The second lossy case is acronyms, and it is entirely self-inflicted. If the backend writes user_uuid and the frontend writes userUUID, the naive reverse transform produces user_u_u_i_d. Both major guidelines close this the same way: Azure says do not upper-case acronyms, and the protobuf style guide says to treat abbreviations as single words — dns_request, not d_n_s_request. So userUuid, xmlHttpRequest, apiKeyId. If you need to audit an existing codebase for screamed acronyms, a pattern like [A-Z]{2,} in a regex tester finds them in one pass.

Verify the round trip before you ship it

Generate a list of every field name in your schema, run it through your transform in both directions, and compare the output against the input. A diff checker makes the failures obvious in seconds — and the ones that show up are exactly the names you should rename before launch, not the transform you should patch.

Naming rules for URLs, query parameters, and headers

JSON bodies are not the only surface, and the conventions differ deliberately.

Path segments prefer kebab-case

The Azure guidelines say to use kebab-casing (preferred) or camel-casing for URL path segments, with one exception: if a segment refers to a JSON field, match that field's camelCase. So /line-items for a collection, but /orders/{id}/lineItems if lineItems is literally the field name being addressed.

Query parameters follow the JSON convention

Azure requires camel case for query parameter names, which keeps ?sortBy=createdAt consistent with the body. Legacy kebab-cased parameters are tolerated only for backwards compatibility. Whatever you choose, remember that the value still needs percent-encoding — a sort_by=created_at desc parameter breaks on the space, not the underscore.

JSON:API adds its own constraints

If you follow JSON:API v1.1, member names are case-sensitive and restricted: hyphen, underscore, and space are allowed but never as the first or last character, and . is reserved because it separates relationship paths. That last one matters if you were planning dotted-path keys such as address.city — JSON:API forbids them outright, and even outside JSON:API they collide with the flattening conventions used when moving data to tabular formats. A CSV ↔ JSON converter will happily turn address.city into a column header, which is exactly the ambiguity JSON:API is avoiding.

Picking a convention and writing it down

The decision is cheap; the drift is expensive. A workable default for a new service in 2026:

  • camelCase for JSON field names, request and response, no exceptions.
  • kebab-case for URL path segments, camelCase for query parameters.
  • Acronyms title-cased (userUuid), never screamed (userUUID).
  • No underscore before a digit, no leading, trailing, or doubled underscores.
  • One conversion point, in the serializer, with the field list visible.

Then generate your client from the schema — OpenAPI, protobuf, or a typed SDK — so the mapping lives in one artifact both sides read, rather than in a regex each side re-invents. The teams that never have this argument are the ones where nobody is converting anything by hand.

References

Related on iKit

Related posts