API Naming Conventions & Style Guide
Naming is the cheapest-to-get-right, most-expensive-to-fix part of an API. A bad name is permanent. Pick a documented style guide, encode it as Spectral rules, and never debate it in code review again.
Searchable Rule Book
API Naming Convention Lookup
β /orders/{orderId}β /getOrder, /order/{id}The HTTP method is the verb; collections are plural so /orders and /orders/42 are consistent.
β /shipping-addressesβ /shippingAddresses, /shipping_addressesURLs are case-sensitive in the path; lowercase hyphenated is readable and SEO/CDN-safe.
β /orders/42/items, /items?orderId=42β /customers/1/orders/42/items/7/pricesDeep nesting couples URLs to hierarchy and breaks when ownership changes; expose sub-resources by top-level id.
β /invoices/42β /invoices/42.json/, /createInvoiceUse Accept for format and methods for actions.
β POST /orders/42:cancel or POST /orders/42/cancellationβ GET /cancelOrder?id=42Non-CRUD state transitions need POST; the colon style follows Google AIP-136.
β pageSize, sortByβ page_size & sortBy mixedConsistency matters more than the choice; enforce with a linter.
β limit + cursor (or pageSize + pageToken)β p, n, start, rowsOpaque cursor tokens let you change storage without breaking clients.
β sort=-createdAt,nameβ orderBy=createdAt DESCLeading minus = descending; never pass raw SQL fragments (injection risk).
β status=PAID&createdAfter=2025-01-01T00:00:00Zβ filter=status eq paid (ad-hoc)Use a documented grammar (RSQL/OData) only if you truly need it; otherwise simple fields.
β firstName, createdAtβ first_name, FirstNameMatches JavaScript and most client SDKs; snake_case is acceptable if used everywhere (Stripe, GitHub).
β "2025-03-01T10:15:30Z"β 1709287530, "03/01/2025"Unambiguous, sortable, timezone-safe. Date-only fields use YYYY-MM-DD.
β {"amount":"1999","currency":"USD"}β {"price":19.99}Binary floating point cannot represent decimals; JavaScript numbers lose precision above 2^53.
β isActive, hasChildrenβ notDisabled, active_flagDouble negatives are error-prone.
β "status":"PENDING_PAYMENT"β "status":2Self-describing and stable; clients must tolerate unknown values (open enums).
β "items":[...], "id":"ord_8f3k"β "item":[...], "id":42 (leaks counts)Opaque string ids prevent enumeration and allow migration to UUID/ULID.
β Idempotency-Key, Retry-After, Sunsetβ X-Idempotency, X-RetryRFC 6648 deprecated the X- prefix; use registered names or an org prefix.
β traceparent (W3C Trace Context)β X-Req-Id invented per teamInteroperates with OpenTelemetry, gateways, and vendors.
β listOrders, getOrderById, cancelOrderβ get_orders_orders_get (auto)Becomes the method name in generated SDKs; instability renames client methods.
β Order, CreateOrderRequest, OrderResponse, ProblemDetailβ order_dto2, Order1Generated class names; separate request/response to avoid readOnly/writeOnly confusion.
β tags: [Orders] summary: "List orders"β tags: [OrderController]Tags drive navigation and SDK grouping, not Java class names.
β $ref: "#/components/responses/NotFound"β Copy/pasted inline error schemasOne definition = one change; prevents drift.
1. URL Design
| Rule | Example | Rationale |
|---|---|---|
| Nouns, not verbs | GET /orders not GET /getOrders | HTTP method carries the verb |
| Plural collections | /orders, /orders/ord_42 | Uniform shape for list vs item |
| kebab-case | /shipping-addresses | Path is case-sensitive; hyphen readable |
| Lowercase only | /orders | Avoid /Orders vs /orders duplicates and cache misses |
| No trailing slash, no extension | /orders/42 | One canonical URL per resource |
| Identify with stable opaque ids | /orders/ord_8f3kd92k | Do not leak sequence/count; prevents enumeration |
| Limit nesting to 2 levels | /orders/42/items | Deep paths couple clients to hierarchy |
| Sub-resources only for true composition | /orders/42/items ok; /customers/7/orders vs /orders?customerId=7 - prefer the filter | A resource should have ONE canonical location |
| Singleton sub-resource singular | /users/me/profile, /orders/42/cancellation | Exactly one per parent |
| Version in a single, consistent place | /v1/... or header | See Versioning |
Non-CRUD Operations (Actions)
REST models state, but real systems have commands. Options, in order of preference:
- Model the action as a resource -
POST /orders/42/cancellationscreates a cancellation (auditable, retrievable, idempotent by key). - Colon custom method (Google AIP-136) -
POST /orders/42:cancel. Compact, clearly not CRUD. - State field update -
PATCH /orders/42with{"status":"CANCELLED"}only when transitions are trivial and validated. - Controller sub-resource verb -
POST /orders/42/cancelacceptable if consistent; neverGETfor state changes.
2. Query Parameters
| Concern | Convention | Example |
|---|---|---|
| Case | Same as JSON fields (camelCase) | pageSize, createdAfter |
| Pagination | Cursor: limit, cursor; or pageSize, pageToken (AIP-158) | ?limit=25&cursor=eyJpZCI6... |
| Sorting | Comma list, - prefix for desc | ?sort=-createdAt,name |
| Filtering | Field name = value; range via suffix/prefix | ?status=PAID&createdAfter=...&createdBefore=... |
| Multi-value | Repeat or comma (document one) | ?status=PAID&status=SHIPPED |
| Sparse fields | fields | ?fields=id,status,total |
| Expansion | expand | ?expand=customer,items.product |
| Search | q | ?q=wireless+headphones |
| Booleans | true/false literals | ?includeDeleted=true |
Reserve these names across every API in the company so SDKs and gateways can treat them uniformly.
3. JSON Property Naming
| Rule | Good | Avoid |
|---|---|---|
| Choose ONE case org-wide (camelCase default; snake_case if you are Stripe/GitHub-style) | firstName | Mixed first_name and lastName |
| Boolean as predicate | isActive, hasChildren, canEdit | active, flag, notDisabled |
Timestamps end in At, dates in On | createdAt, dueOn | created, date1 |
| Durations include unit or use ISO-8601 | timeoutSeconds, "PT15M" | timeout: 30 |
| Arrays plural | items, tags | item, tagList |
Foreign keys end in Id | customerId | customer (ambiguous id vs object) |
| Embedded object vs id | customer: {id, name} when expanded | Changing the type of a field between calls |
| Money | { "amount": "1999", "currency": "USD" } or integer minor units | 19.99 float |
| No abbreviations unless universal | quantity, url, id | qty, desc, addr1 |
| No redundant prefix | order.status | order.orderStatus |
| Null vs absent documented | absent = unchanged (PATCH), null = clear | Treating them identically |
Identifiers
| Style | Example | Notes |
|---|---|---|
| Prefixed random | ord_8f3kd92k (Stripe) | Self-describing in logs; safe to expose |
| UUIDv4 | 3f2b... | Opaque; poor B-tree locality as PK |
| UUIDv7 / ULID | 01HV... | Time-ordered: better index locality, leaks creation time |
| Auto-increment | 12345 | Never expose: enumeration, count leaks, BOLA targets |
Ids are strings in JSON even if numeric internally (JavaScript loses precision above 2^53).
Enums
- Strings in
UPPER_SNAKE_CASE:PENDING_PAYMENT(or lowercasepending_paymentconsistently). - Open enum rule: clients must treat unknown values as "other" - adding a value must not be a breaking change. Declare this in the description.
- Never use ordinal numbers; they cannot be reordered or reused.
4. HTTP Headers
| Purpose | Use | Don't |
|---|---|---|
| Idempotency | Idempotency-Key (IETF draft) | X-Idempotency-Token |
| Tracing | traceparent, tracestate (W3C) | X-Request-Trace-Id-V2 |
| Rate limits | RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset, Retry-After | Bespoke names per team |
| Deprecation | Deprecation, Sunset (RFC 8594), Link: rel="successor-version" | Docs-only notice |
| Pagination | Link header (RFC 8288) or body nextCursor | Undocumented X-Next |
| Concurrency | ETag, If-Match, If-None-Match | version query param |
| Custom headers | Org prefix without X- (RFC 6648), e.g. Acme-Tenant-Id | X- prefix for new headers |
Header names are case-insensitive; docs use Title-Case.
5. OpenAPI Document Naming
| Element | Convention | Example |
|---|---|---|
operationId | camelCase verbNoun, unique, stable | listOrders, getOrderById, cancelOrder |
| Tags | Resource names, PascalCase/Title | Orders, Shipping Addresses |
| Schemas | PascalCase nouns with role suffix | Order, CreateOrderRequest, UpdateOrderRequest, OrderPage, ProblemDetail |
| Parameters (components) | PascalCase | Limit, Cursor, IdempotencyKey |
| Responses (components) | HTTP semantic names | NotFound, Unauthorized, ValidationError |
| Security schemes | camelCase | bearerAuth, oauth2 |
| Files | openapi.yaml, split by domain: orders.openapi.yaml | Keep one spec per deployable API |
operationId verbs: list (collection GET), get (single), create (POST), replace (PUT), update (PATCH), delete (DELETE), then domain verbs (cancel, capture, refund).
6. Style Guide Comparison
| Topic | Google AIP | Microsoft REST | Zalando | Stripe |
|---|---|---|---|---|
| Path case | lowerCamelCase collection ids (userEvents) | camelCase/kebab | kebab-case | snake_case (payment_intents) |
| Property case | camelCase (JSON) | camelCase | snake_case | snake_case |
| Custom methods | POST /v1/resources/id:verb | POST .../action | sub-resource | POST /v1/charges/id/capture |
| Pagination | pageSize, pageToken | $top, $skip, nextLink | cursor limit/cursor | limit, starting_after |
| Errors | google.rpc.Status | error{code,message} | RFC 7807 problem | error{type,code,message,param} |
| Versioning | Major in path, stable | Path or query | Media-type versioning | Date-based header Stripe-Version |
| Enums | UPPER_SNAKE | PascalCase string | lower_snake | lowercase_snake |
There is no universal winner; copy a mature guide, then diverge in writing, enforcing it with Spectral.
7. Spectral Ruleset Example
# .spectral.yaml
extends: ["spectral:oas"]
rules:
paths-kebab-case:
description: Path segments must be kebab-case
severity: error
given: $.paths[*]~
then:
function: pattern
functionOptions:
match: "^(/([a-z0-9]+(-[a-z0-9]+)*|\\{[a-zA-Z0-9]+\\}))+$"
property-camel-case:
description: Schema properties must be camelCase
severity: error
given: $..properties[*]~
then:
function: casing
functionOptions: { type: camel }
operation-id-camel-case:
severity: error
given: $.paths[*][*].operationId
then:
function: casing
functionOptions: { type: camel }
error-responses-problem-json:
description: 4xx/5xx must use application/problem+json
severity: warn
given: $.paths[*][*].responses[?(@property.match(/^[45]/))].content
then:
field: application/problem+json
function: truthy
8. Naming Review Checklist
- Every path is a plural noun in kebab-case with at most two levels
- No verbs in paths except documented custom actions
- All property names follow one case; booleans are predicates; timestamps end with
At - Ids are opaque strings; enums are strings and open
- Money, durations, and dates use unambiguous formats
-
operationIds are unique, stable, and verb-first - Standard parameter names (
limit,cursor,sort,fields,expand) reused - Headers use standard names or an org prefix (no
X-)
