Good API naming conventions fit on one card. Paths are lowercase, plural nouns with hyphens between words (/v1/user-profiles), and the HTTP method carries the action, so no verbs in the path. Path parameters get short, unique names such as {id} or {orderId}. JSON fields use one casing across the whole API, either camelCase or snake_case, and query parameters use the same casing as the fields they filter. Booleans start with is or has, timestamps end in At and carry a timezone, and numbers with units say the unit. New HTTP headers drop the X- prefix. The rest of this page explains each rule, the exceptions, and how to rename things without breaking clients.
Naming cheat sheet
| Element | Convention | Good | Avoid |
|---|---|---|---|
| Collection path | Lowercase plural noun | /v1/orders | /v1/Order, /v1/getOrders |
| Multi-word segment | kebab-case | /v1/user-profiles | /v1/userProfiles, /v1/user_profiles |
| Single resource | Collection plus ID | /v1/orders/{id} | /v1/order/{id}, /v1/orders/get/{id} |
| Non-CRUD action | Sub-resource noun | POST /v1/orders/{id}/cancellation | POST /v1/cancelOrder |
| JSON field | One casing everywhere | customerId or customer_id | Both in one API |
| Boolean field | is or has prefix | isActive, hasMore | active, more |
| Timestamp field | At suffix, ISO 8601 with offset | createdAt | created, creationDate |
| Value with a unit | Unit in the name | timeoutSeconds, totalCents | timeout, total |
| Enum value | One casing for all enums | in_progress | InProgress next to on-hold |
| Query parameter | Same casing as JSON fields | ?createdAfter= | ?created_after= in a camelCase API |
| New HTTP header | Hyphenated words, no X- prefix | Request-Id | X_Request_ID, requestid |
Paths: lowercase, plural, no verbs
RFC 3986 treats the scheme and host of a URI as case-insensitive but the path as case-sensitive, so /v1/Orders and /v1/orders are two different URLs. Some servers and frameworks match routes case-insensitively and others do not, and caches and gateways key on the exact string. Lowercase-only paths remove the question. Between words, use hyphens. Hyphens are the common choice in URLs, they read clearly when a link is underlined, and they keep the path visually separate from snake_case or camelCase JSON.
Name collections with plural nouns and address one item by appending its ID: /v1/orders and /v1/orders/{id}. Mixing /user and /users in the same API forces client developers to look up each path. The HTTP method already says what happens: GET reads, POST creates, PATCH updates part of a resource, DELETE removes it. A path such as /v1/orders/create repeats the method and invites the inconsistency of POST /create next to PUT /update. The REST API design guide covers method semantics in more depth.
Some operations are not create, read, update or delete. Cancelling an order, sending an invoice or resending a verification email are actions. Model them as a sub-resource noun that you create: POST /v1/orders/{id}/cancellation or POST /v1/invoices/{id}/delivery. That keeps the path a noun, gives you a place to return the cancellation record, and works with the same tooling as every other POST.
Keep nesting to one level of ownership. /v1/users/{userId}/orders reads well. /v1/users/{userId}/orders/{orderId}/line-items/{lineItemId} makes the client know the user ID to read a line item, even though the order ID already identifies it. If a child has a globally unique ID, give it a top-level path such as /v1/line-items/{id} and offer filtering by parent with a query parameter.
Path parameter names
OpenAPI identifies a parameter by its name and location together, and every template expression in a path must match a declared path parameter. Two path parameters in one path therefore need different names, which is why nested routes use {userId} and {orderId} instead of {id} twice. The spec also forbids templated paths that differ only in the parameter name: /orders/{id} and /orders/{orderId} in the same document count as the same path and make the document invalid.
For a single parameter, {id} is enough. Use the same casing for parameter names as for your JSON fields, so {orderId} in a camelCase API and {order_id} in a snake_case one. One thing to know if you track the APIForge score: its naming check reads the whole path string, so camelCase or underscores inside a parameter name count against kebab-case. In our example below, renaming {id} to {userId} on two operations moved the score from 100 to 95.5. Treat that deduction as a prompt to check the name, not as a rule that {userId} is wrong.
JSON field names: one casing, chosen once
camelCase and snake_case both work. camelCase matches JavaScript and TypeScript property names, so frontend code can destructure a response without a mapping layer. snake_case matches Python and Ruby conventions and many database column names. Pick the one your main consumers write, set your serializer to emit it everywhere, and add a test that fails if a response contains the other style. An API that returns customer_id from one endpoint and customerId from another costs every client a lookup per field.
AvoidFour casings and no units
{
"ID": "ord_81",
"customer_id": "usr_12",
"OrderStatus": "InProgress",
"active": true,
"created": 1767225600,
"timeout": 30,
"total": 1999
}ID, customer_id and OrderStatus use three casings. active could be a verb or a state. created is a Unix timestamp with no hint of the unit or timezone. timeout and total leave the client to guess seconds or milliseconds, dollars or cents.
PreferOne casing, self-describing names
{
"id": "ord_81",
"customerId": "usr_12",
"status": "in_progress",
"isActive": true,
"createdAt": "2026-01-01T00:00:00Z",
"timeoutSeconds": 30,
"totalCents": 1999
}Every key is camelCase. The boolean reads as a question, the timestamp is ISO 8601 with a UTC offset, units sit in the names, and the enum value uses snake_case like every other enum in the API.
- Acronyms: treat them as words. userId, htmlUrl and apiKey are easier to convert between casings than userID or HTMLURL, and code generators handle them predictably.
- Do not repeat the resource name. Inside an order object, write status, not orderStatus. Keep the prefix only for references to other resources, such as customerId.
- Money: store an integer in the smallest unit with the unit in the name, such as totalCents, plus a currency field with an ISO 4217 code.
- Arrays: plural names (lineItems, tags). Single objects: singular names (shippingAddress).
- Enums: one casing for every enum value in the API. Changing in_progress to inProgress later is a breaking change for every client that compares strings.
Query parameters
Query parameters should look like the fields they filter. If the response has status and createdAt, the filters are status and createdAfter or createdBefore, not state and from_date. Reserve a few names and use them on every list endpoint: limit and cursor (or page) for pagination, sort with a minus prefix for descending order, and fields if you support sparse responses.
GET /v1/orders?status=paid&createdAfter=2026-01-01T00:00:00Z&sort=-createdAt&limit=50&cursor=b3JkXzgx
GET /v1/orders?status=paid&status=refunded
GET /v1/products?ids=prd_1,prd_2,prd_3For multiple values, choose repeated parameters (status=paid&status=refunded) or a comma-separated list (ids=prd_1,prd_2) and use the same form everywhere. In OpenAPI 3, style: form with explode: true describes the repeated form and explode: false describes the comma form, so generated clients serialize them correctly.
HTTP header names
RFC 9110 defines header field names as case-insensitive, so Request-Id and request-id are the same header and a client must not depend on the case it receives. Write them in documentation as hyphen-separated words with initial capitals, and never use underscores, because some proxies drop headers that contain them (nginx does by default). RFC 6648 (BCP 178) asks protocol designers to stop prefixing new parameters with X-, because experimental names leak into permanent use and then cannot be renamed. For a new header, write Request-Id or Idempotency-Key. If clients already send X-Request-ID, keep accepting it; renaming a working header breaks them for no gain.
Worked example: scoring a messy spec
Here is a small spec with naming problems that show up often: a verb in a collection path, a singular resource name, a snake_case segment and an action segment.
openapi: 3.0.3
info:
title: Shop API
version: 1.0.0
paths:
/v1/getUsers:
get:
summary: List users
responses:
"200":
description: A page of users
"401":
description: Missing or invalid token
/v1/user/{user_id}:
get:
summary: Get one user
parameters:
- name: user_id
in: path
required: true
schema:
type: string
responses:
"200":
description: The user
"404":
description: User not found
/v1/user_profiles/{id}:
patch:
summary: Update a profile
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"200":
description: The updated profile
"404":
description: Profile not found
/v1/orders/create:
post:
summary: Create an order
responses:
"201":
description: Order created
"400":
description: Invalid orderAPIForge scored it 86.6 out of 100. Naming dropped to 8.8 of its 20 points and HTTP usage to 17.8 of 20. It reported camelCase in /v1/getUsers, the verb segment create in /v1/orders/create (flagged twice, once as naming and once as a method conflict), an underscore in user_profiles, and the underscore in the {user_id} parameter. It did not flag the singular /v1/user, because its singular-versus-plural check fires only when both /user and /users appear as segments, and getUsers is not a users segment. A rule-based checker catches what its rules describe, so a human still needs to read the paths.
openapi: 3.0.3
info:
title: Shop API
version: 1.0.0
paths:
/v1/users:
get:
summary: List users
responses:
"200":
description: A page of users
"401":
description: Missing or invalid token
/v1/users/{id}:
get:
summary: Get one user
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"200":
description: The user
"404":
description: User not found
/v1/user-profiles/{id}:
patch:
summary: Update a profile
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"200":
description: The updated profile
"404":
description: Profile not found
/v1/orders:
post:
summary: Create an order
responses:
"201":
description: Order created
"400":
description: Invalid orderAfter renaming to /v1/users, /v1/users/{id}, /v1/user-profiles/{id} and POST /v1/orders, the score is 100 with no issues. The score does not look at JSON field names or query parameters, so check those by hand against the cheat sheet above.
Renaming without breaking clients
Every rename in a published API is a breaking change. Clients have the old path or field name compiled into their code. Roll renames out in three steps: add the new name next to the old one, mark the old one deprecated in the spec (OpenAPI has deprecated: true on operations, parameters and schemas) and tell consumers when it goes away, then remove it in the next major version. For fields, return both names during the overlap. The API versioning guide covers deprecation windows and how to announce them.
Renames are cheapest before the first release. Write the conventions down in your repository, link them from the spec's info.description, and check new paths in review before the first client depends on them.
Check your paths in APIForge
Paste a spec URL or file into the API quality checker. It scores the API from 0 to 100 across seven categories, with naming worth 20 points, and lists each path that uses camelCase, underscores or verb segments, along with mixed singular and plural resource names. The glossary entry on REST APIs covers the resource model these naming rules come from.