Naming conventions for APIs, JSON, databases and files
By Uxair Ijaz · 2026-11-06 · 8 min read
Naming is the cheapest refactoring and the most expensive mistake. A renamed column costs a migration; a renamed API field costs every client; an inconsistent scheme costs a little confusion every single day, forever. This guide sets out one coherent scheme across APIs, JSON, databases and code — with the reasoning, so you can deviate deliberately instead of accidentally.
URL paths: plural nouns, kebab-case, no verbs
GET /orders ✓
GET /order ✗ singular
GET /getOrders ✗ verb
GET /Orders/42 ✗ capitals
GET /order_items/9 ✓ (or /order-items/9 — pick one, see below)
GET /orders/42/items ✓ nested for ownership, two levels max
The rules professionals keep:
- Plural collections, singular-with-ID resources:
/orders/42. Reads like English: "order 42 from the orders". - Methods are the verbs.
POST /orders/42/cancellationis legitimate for non-CRUD actions;POST /cancelOrder?id=42is RPC in a trench coat. - kebab-case (hyphenated) paths dominate modern APIs —
invoice-templates, notinvoiceTemplatesorinvoice_templates. Hostnames are case-insensitive; paths are not, so all-lowercase paths remove a whole class of 404s. - Hierarchy is ownership, not composition:
/customers/7/orders("customer 7's orders"), then flatten below that level. - Versions in the path:
/v1/orders. Visible in logs, trivially routed, honest about being a deployment boundary.
One genuine fork: hyphens or underscores in paths? Both appear in serious APIs (GitHub uses hyphens, AWS uses underscores in places). Hyphens edge it: they never need %-encoding discussion, they read better, and they cannot collide with query-parameter conventions. Choose per API, never mix.
JSON fields: camelCase or snake_case?
The two live conventions:
{ "createdAt": "2026-11-06T09:30:00Z", "orderId": "42" } // camelCase
{ "created_at": "2026-11-06T09:30:00Z", "order_id": "42" } // snake_case
Who uses which: Google, Facebook and most JavaScript-first shops use camelCase; Stripe, Twitter/X, AWS and Python-first shops use snake_case.
The deciding facts:
- JavaScript consumes JSON by default, and camelCase needs no mapping there (
body.orderIdjust works). Snake_case JSON in a TypeScript codebase grows a conversion layer or a field-access style that quietly forks the codebase. - snake_case is easier to read when values are dense acronyms:
tax_id_numbervstaxIdNumber. - Whatever you choose, do not mix — and that includes acronyms:
orderId, neverorderIDin camelCase;user_id, neveruserIDin snake_case.
What professionals choose: camelCase for JSON APIs whose main clients are browsers and JavaScript services; snake_case when the API is Python/Data-flavoured or mirrors an existing snake_case system. Either is fine on day one and expensive on day 300 — decide before the first field ships.
Two more field rules that prevent real bugs:
- Booleans read as predicates:
"is_active","has_banner","paid". Avoid bare nouns ("active": trueinvites "active what?"). - No verbs in field names:
"is_cancelled": true, not"cancelled_at_time_of_writing": ...— and timestamps end in_at(created_at,cancelled_at), durations end in_seconds/_ms.
Query parameters: the reserved words
Standardise these from day one, because every endpoint will grow them eventually:
| Parameter | Purpose | Example |
|---|---|---|
limit / cursor (or page/offset) |
pagination | ?limit=20&cursor=abc |
sort |
ordering, - for descending |
?sort=-created_at |
q |
free-text search | ?q=blue |
status, since, until |
filters | ?status=placed&since=2026-11-01 |
Match their case to your JSON convention (created_at sorts a snake_case API; createdAt a camelCase one) — clients copy field names into sort/filter params constantly, and a mismatch there is a guaranteed support ticket.
Databases: snake_case all the way
Postgres, MySQL and most ORMs converge on the same conventions, and they differ from your API's on purpose — the API layer is the translator:
- Tables plural, snake_case:
orders,order_items,users. - Columns snake_case:
created_at,total_cents,status. - Primary key
id; foreign keys<singular>_id:customer_id. - Timestamps:
created_atandupdated_aton every table,timestamptz, no exceptions. - Constraint names follow what the error message will show:
pk_orders,fk_orders_customer_id,uq_users_email,idx_orders_created_at. When production pages you at 3 a.m.,uq_users_email violatedis the diagnosis, notconstraint_7 violated. - Booleans:
is_/has_prefixes (is_cancelled), no bare adjectives.
Why translate at the edge? Because each world's defaults win locally: the database community reads snake_case best, the JavaScript community reads camelCase best, and the mapping lives in exactly one layer (the ORM/serialiser) instead of leaking into both.
Code and files: follow the ecosystem, not your taste
Every ecosystem already voted; the professional move is to comply:
| Ecosystem | Files | Symbols |
|---|---|---|
| Angular, Angular-style TS | kebab-case (invoice-generator.ts) |
camelCase classes (InvoiceGenerator) |
| React/Next | PascalCase components (InvoiceForm.tsx) or kebab, per repo |
PascalCase components, camelCase functions |
| Python | snake_case (invoice_generator.py) |
snake_case functions, PascalCase classes |
| Go | snake_case | MixedCaps, no underscores |
| Java | PascalCase files matching the class | camelCase methods |
| CSS/utility classes | kebab-case (invoice-card) |
— |
Mixed-ecosystem repos pick per directory and write the rule in the README — one CONTRIBUTING.md paragraph prevents a hundred pull-request comments.
HTTP headers and other strangers
- Headers:
Content-Type,X-Request-Id— hyphenated, case-insensitive by spec (send them capitalised anyway; proxies and log parsers read case). - Environment variables: SCREAMING_SNAKE_CASE (
DATABASE_URL,JWT_SECRET) — universal, don't innovate. - Query string encoding: names in your chosen convention; values URL-encoded (why
%20appears — and whatencodeURIvsencodeURIComponentactually do). - Git branches:
type/ticket-description(fix/231-invoice-total), kebab-case — sortable, greppable.
The one-page scheme
Copy this into your team wiki and you have adopted the majority position of the industry:
| Layer | Convention | Example |
|---|---|---|
| URL paths | plural nouns, kebab-case, /v1/ prefix |
/v1/invoice-templates/9 |
| JSON fields | camelCase (or snake_case — one only) | createdAt |
| Query params | matches JSON convention; reserved: limit, cursor, sort, q |
?sort=-createdAt |
| Database | snake_case, plural tables, _id FKs, created_at everywhere |
orders.created_at |
| Constraints | pk_ / fk_ / uq_ / idx_ + table + column |
uq_users_email |
| Env vars | SCREAMING_SNAKE_CASE | JWT_SECRET |
| Headers | Hyphenated, capitalised | X-Request-Id |
The rule underneath all the rules
Every convention here serves one of two masters: the reader (kebab-case paths, created_at, uq_users_email) and the machine (stable error types, reserved query words, one case per layer). When you face a case this guide did not cover, ask which master the name serves — and if the answer is "neither", the name is fine; stop deliberating and ship.
Need to check what a field is called everywhere else before you pick? The case converter flips a name between camelCase, snake_case, kebab-case and friends, and the JSON formatter shows your real-world payloads in the convention you actually shipped — which is sometimes not the one in this table.