REST API Naming Conventions: A Practical Style Guide

Master REST API naming conventions with 10 concrete rules: plural nouns, kebab-case paths, JSON casing, versioning, and IDs. Do and don't examples included.

Ashley Goolam

Ashley Goolam

31 August 2026

REST API Naming Conventions: A Practical Style Guide

Apidog for Enterprise

On-Premises Deploy

SSO & RBAC

SOC 2 Compliant

Explore Apidog Enterprise

Open any codebase older than two years and you’ll find the scars: /getUser, /user_list, /Users/fetchAll, three different pagination schemes, and a customerID field sitting next to order_id in the same response. None of it breaks anything. All of it slows everyone down.

Naming is the cheapest API design decision you’ll ever make and the most expensive one to reverse. Once clients depend on /getOrders, you’re stuck supporting it for years. This guide gives you a concrete rule for every naming decision a REST API forces on you, with an example and an anti-example for each. It follows the same thinking as our broader REST API guidelines for developers, but zooms in on the part teams fight about most: what to call things.

If you’d rather enforce these rules with tooling than with code review comments, Apidog lets you define every endpoint visually against a shared schema before anyone writes code. More on that at the end.

Use plural nouns for collections

A URL names a resource, not an operation. Collections are sets of things, so name them as plural nouns.

Do:

GET /v1/products
GET /v1/products/89
GET /v1/orders

Don’t:

GET /v1/getProducts
GET /v1/product
GET /v1/productList

The plural form works at both levels. /products reads as “the collection of products” and /products/89 reads as “product 89 within the collection.” Singular naming forces awkward URLs like /product/89 for one item but /product for many, which reads wrong. The Microsoft REST API guidelines settled on plural nouns for exactly this reason, and most public APIs (Stripe, GitHub, Shopify) followed the same path.

One exception: singleton resources. If a user has exactly one cart, /users/42/cart is fine. Don’t pluralize something with a cardinality of one.

Keep verbs out of paths

The HTTP method is the verb. Putting another verb in the path duplicates information and breaks the resource model.

Do:

GET    /v1/orders/42      (read it)
DELETE /v1/orders/42      (delete it)
PATCH  /v1/orders/42      (update it)

Don’t:

GET  /v1/fetchOrder/42
POST /v1/deleteOrder/42
POST /v1/updateOrderStatus

Verb-based paths also multiply your surface area. One resource with four methods becomes four endpoints to document, test, and cache separately. Cache invalidation gets worse too: a CDN can cache GET /v1/orders/42 and invalidate on DELETE /v1/orders/42 because both point at the same URL. It can’t connect /fetchOrder/42 to /deleteOrder/42.

Use kebab-case in URL paths

Multi-word path segments need a separator, and hyphens are the right one.

Do:

/v1/gift-cards
/v1/shipping-addresses

Don’t:

/v1/giftCards
/v1/gift_cards
/v1/GiftCards

Three reasons. Google treats hyphens as word separators for indexing, so public API docs rank better with kebab-case. Underscores disappear when a URL gets underlined in an email or doc. And camelCase in URLs invites case-sensitivity bugs: /giftCards and /giftcards are different URLs on most servers, and someone will type the wrong one. The Zalando RESTful API guidelines make kebab-case a MUST rule, and they’ve run this playbook across hundreds of internal services.

Pick one JSON casing and write it down

For field names inside request and response bodies, the honest answer is: camelCase and snake_case both work. What doesn’t work is mixing them.

Do (either one, consistently):

{ "orderId": 42, "createdAt": "2026-08-30T09:15:00Z", "totalAmount": 4999 }
{ "order_id": 42, "created_at": "2026-08-30T09:15:00Z", "total_amount": 4999 }

Don’t:

{ "orderId": 42, "created_at": "2026-08-30T09:15:00Z", "TotalAmount": 4999 }

camelCase maps cleanly to JavaScript and Java clients. snake_case is easier to scan and matches Ruby, Python, and most SQL column names; Stripe uses it everywhere. Pick based on who consumes your API most, then put the choice in your style guide so the debate happens once instead of on every pull request. Mixed casing is the single most common inconsistency in real-world APIs because different teams ship different endpoints. That’s a governance failure, not a taste failure.

Limit nesting to two levels

Nesting expresses ownership: /users/42/orders means “orders belonging to user 42.” That’s useful. Past two levels it stops being useful.

Do:

GET /v1/users/42/orders
GET /v1/orders/1337/refunds

Don’t:

GET /v1/users/42/orders/1337/refunds/7/status

Deep nesting forces clients to carry every ancestor ID to reach a leaf resource, even when the leaf has a globally unique ID of its own. If a refund has ID 7, expose it at /refunds/7 or /orders/1337/refunds/7 and stop there. A good smell test: if a URL contains three or more IDs, flatten it. Once an order exists, it doesn’t need its user in the path; /orders/1337 stands on its own.

Put filtering, sorting, and pagination in query parameters

Paths identify resources. Query parameters modify how you view them. Never encode a filter into the path.

Do:

GET /v1/orders?status=active&sort=-created_at&limit=50&cursor=eyJpZCI6NDJ9
GET /v1/products?category=electronics&min_price=1000

Don’t:

GET /v1/orders/active
GET /v1/orders/sorted-by-date-desc
GET /v1/getOrdersByStatusAndDate

The sort=-created_at pattern (minus prefix for descending) comes from the JSON:API spec and saves you a second order=desc parameter. Filter paths like /orders/active look harmless until you need to combine filters, and then you’re minting a new endpoint per combination. Pagination parameter names deserve the same discipline: pick limit/cursor or page/per_page once and reuse them on every collection. Our API pagination guide covers the cursor-versus-offset trade-off in depth; the naming rule here is simply to be uniform about it.

Version in the path

You have two mainstream options: a path segment (/v1/products) or a header (Accept: application/vnd.myapi.v1+json). Header versioning is more “pure” REST, since the URL keeps naming the same resource across versions, and the Google API design guidance notes both approaches exist in the wild. But path versioning wins on operational grounds: it’s visible in every log line, testable from a browser, cacheable without Vary gymnastics, and impossible for a client to forget. Every developer who’s debugged a “works in curl, fails in prod” issue caused by a missing version header knows the cost of the alternative. Use /v1/ with a major version only, no /v1.2/; minor changes should be additive and non-breaking. For the full decision tree, including content negotiation, see our comparison of API versioning strategies.

Treat resource IDs as opaque, and don’t leak sequential integers carelessly

/orders/41, /orders/42, /orders/43: sequential integer IDs tell anyone who looks exactly how many orders you process, and they invite enumeration attacks where an attacker walks the ID space probing for authorization gaps. This class of bug, broken object level authorization, sits at number one on the OWASP API Security Top 10.

Do:

GET /v1/orders/ord_9f8e2a71b3
GET /v1/users/550e8400-e29b-41d4-a716-446655440000

Don’t (when enumeration matters):

GET /v1/orders/42
GET /v1/invoices/10883

Prefixed random IDs like Stripe’s ord_9f8e2a71b3 are the strongest pattern: unguessable, self-describing in logs, and safe to expose. Authorization checks are still mandatory either way. Opaque IDs reduce the blast radius of a missing check; they don’t replace it. Internally you can keep integer primary keys; the rule is about what you expose in URLs.

Model non-CRUD actions as controller resources

Sooner or later you need an action with no clean CRUD mapping: cancel an order, retry a payment, resend an email. Don’t tunnel it through PATCH on a status field, and don’t put a verb at the top level.

Do:

POST /v1/orders/42/cancel
POST /v1/payments/pay_88a1/retry

Don’t:

PATCH /v1/orders/42        { "status": "cancelled" }
POST  /v1/cancelOrder      { "orderId": 42 }

This is the controller pattern, and it’s the one sanctioned exception to the no-verbs rule: the verb goes at the end of the path, scoped under the resource it acts on. The PATCH approach looks RESTful but hides a state machine inside a field update. Cancelling an order triggers refunds, releases inventory, and sends notifications; pretending it’s a field write forces your server to diff payloads to detect intent. A /cancel endpoint states the intent, gives the action its own permissions and audit trail, and leaves room for action-specific inputs like a cancellation reason.

Keep casing consistent for headers and query parameters

Two smaller surfaces, same discipline. Custom headers use Hyphenated-Pascal-Case, matching HTTP convention: Idempotency-Key, Request-Id. Skip the old X- prefix; it was deprecated by RFC 6648 in 2012. Header names are case-insensitive on the wire, but your docs and SDKs should still spell them one way.

Query parameters should match your JSON body casing. If your bodies use snake_case, write ?min_price=1000&created_after=2026-01-01, not ?minPrice=1000. A developer who reads created_at in a response and must type createdAfter in a query will get it wrong on the first try, and so will everyone after them.

The full rule set at a glance

# Rule Do Don’t
1 Plural nouns for collections /products, /products/89 /getProducts, /productList
2 No verbs in paths DELETE /orders/42 POST /deleteOrder/42
3 kebab-case path segments /gift-cards /giftCards, /gift_cards
4 One JSON casing, documented order_id everywhere orderId and order_id mixed
5 Max two nesting levels /orders/1337/refunds /users/42/orders/1337/refunds/7
6 Filters and pagination in query params ?status=active&sort=-created_at /orders/active
7 Major version in the path /v1/products /v1.2/products, version headers
8 Opaque resource IDs /orders/ord_9f8e2a71b3 /orders/42 (public, enumerable)
9 Controller pattern for actions POST /orders/42/cancel PATCH with {"status":"cancelled"}
10 Consistent header and param casing Idempotency-Key, ?min_price= X-IDEMPOTENCY_KEY, ?minPrice= mixed in

Enforcing conventions at scale

A style guide in a wiki changes nothing. The teams whose APIs stay consistent share one habit: they design first and enforce the conventions before code exists, which is the core of API governance in practice.

This is where Apidog earns its place in the workflow. Endpoints are defined in a schema-first visual designer, so the path, casing, and parameter names are explicit design artifacts instead of strings buried in controller code. Shared components mean Pagination, Error, and Money schemas get defined once and reused across every endpoint; nobody re-invents per_page as pageSize on a new service. And because designs live in team workspaces with review built in, a lead can catch /getUserOrders at design time, when renaming costs one click, instead of after three clients have integrated against it. The spec then drives docs, mock servers, and tests, so the names you approved are the names everyone ships. Download Apidog and try it free with your next new endpoint; retrofitting an old API is hard, but holding the line on new ones is not.

FAQ

Should REST URLs be plural or singular?

Plural, for any resource with more than one instance: /products, /orders, /users. The plural form stays natural for both the collection (/orders) and one member (/orders/42). Reserve singular names for true singletons like /users/42/cart. If you want the deeper reasoning behind resource modeling, our guide on what a REST API is walks through it from first principles.

Is camelCase or snake_case better for JSON field names?

Neither wins on merit. camelCase suits JavaScript-heavy consumers; snake_case is more readable and matches Python, Ruby, and Stripe’s public API. The rule with teeth: choose one, write it into your style guide, and enforce it in schema review. Mixed casing across endpoints hurts more than either choice.

Should I put the API version in the URL or a header?

Use the path (/v1/orders) unless you have a strong hypermedia requirement. Path versions show up in logs, caches, and browser tests with zero client effort. Header versioning keeps URLs stable across versions but fails silently when clients forget the header. Major versions only; ship minor changes as additive, non-breaking updates.

Are verbs ever acceptable in a REST API path?

Yes, in one place: controller endpoints for non-CRUD actions, like POST /orders/42/cancel or POST /payments/pay_88a1/retry. The verb sits at the end of the path, scoped under its resource, and the method is always POST. Everywhere else, the HTTP method carries the verb and the path stays nouns-only.

Explore more

API Caching with ETag and Cache-Control: How Conditional Requests Cut Your Payloads

API Caching with ETag and Cache-Control: How Conditional Requests Cut Your Payloads

Learn how the Cache-Control header and ETag validation turn repeat API calls into 304 responses, prevent lost updates with If-Match, and cut payload size.

31 August 2026

How to Test OAuth 2.0 APIs in Apidog (Authorization Code, Client Credentials, and Token Refresh)

How to Test OAuth 2.0 APIs in Apidog (Authorization Code, Client Credentials, and Token Refresh)

Learn how to test OAuth 2.0 APIs in Apidog: authorization code flow with PKCE, client credentials, automatic token refresh, and 401/403 failure-path tests.

31 August 2026

Cursor-Based Pagination vs Offset Pagination: Which One Should Your API Use?

Cursor-Based Pagination vs Offset Pagination: Which One Should Your API Use?

Cursor-based pagination vs offset pagination compared: page drift, deep-offset cost, keyset SQL, Stripe and Slack examples, and how to test both in Apidog.

31 August 2026

Practice API Design-first in Apidog

Discover an easier way to build and use APIs

REST API Naming Conventions: A Practical Style Guide