Back to Blog
EngineeringOctober 24, 202613 min read

REST API URL Naming Conventions: 7 Rules With Examples

Plural or singular, hyphens or camelCase, path or query, how deep to nest, how to page and bulk delete: the REST URL rules from RFCs and Microsoft, Google and Zalando, with before/after URLs.

REST API URL Naming Conventions: 7 Rules With Examples

11 min read · Last updated: October 25, 2026

REST API URL naming conventions come down to five rules. Use plural nouns for collections (/orders) and lowercase, hyphenated paths (/order-items). Put the ID in the path to point at one resource (/orders/42). Use query parameters to filter, sort and page (/orders?status=shipped&limit=25). Never nest deeper than collection/item/collection.

Came here from Instagram, LinkedIn, X or YouTube? Here is the full source list you came for. Everything below is built from these, nothing else.

The specs

The style guides

The real APIs

The SEO note

Key takeaways

  • All 3 style guides used here (Microsoft, Google, Zalando) say collections get plural nouns.
  • RFC 3986 treats the URL path as case-sensitive, so /Users and /users are two different URLs. Pick lowercase.
  • Microsoft says avoid anything deeper than collection/item/collection. Zalando caps nesting at 3 levels.
  • Stripe caps a page at 100 items (default 10). GitHub caps per_page at 100 on most endpoints.
  • Zalando says you should prefer cursor pagination over offset, because offsets skip or repeat rows when data changes.
  • RFC 9110 says a body on a DELETE request has no generally defined semantics, so bulk delete usually goes through POST.

What are REST API URL naming conventions, in one picture?

A REST URL works like a library. The path is where the book sits, and the query string is what you tell the librarian. /books is the shelf, /books/42 is one book, and ?language=es&sort=newest is "only Spanish, newest first." Paths identify things. Queries describe how you want to see them.

Keep that picture in your head and most design arguments settle themselves.

The spec backs it up. RFC 3986, the standard that defines URI syntax, says the path holds data "usually organized in hierarchical form," while the query "contains non-hierarchical data." So a shelf inside a section inside a building goes in the path. A filter like "only Spanish" has no hierarchy. It goes in the query.

If you are new to REST itself, start with our REST API explainer for beginners and come back.

Here is the whole idea as before and after URLs:

Before (common mistake)After (convention)Rule
/getOrdersGET /ordersNouns in the path, verbs in the HTTP method
/order/42/orders/42Plural collection names
/OrderItems/order-itemsLowercase, hyphen-separated
/orders/status/shipped/orders?status=shippedFilters go in the query
/customers/1/orders/99/products/orders/99/productsMax collection/item/collection
/orders?page=3 on a growing table/orders?limit=25&cursor=abcPrefer cursors
/deleteOrders?ids=1,2,3POST /orders:batchDeleteBulk actions as a separate request

Should REST resource names be singular or plural?

Use plural. Microsoft's API design guide says to "use plural nouns to name collection URIs," Zalando's rule 134 says you "MUST pluralize resource names," and Google's AIP-122 says a collection identifier must be the plural form of the noun. So /customers is the collection and /customers/5 is one customer inside it.

The argument for singular sounds neat (/customer/5 reads like "customer five"). It breaks down the moment you list things, because now you need both /customer and /customers, or a singular name that returns many. Plural gives you one prefix for both.

There is one honest exception. When a resource can only ever be one thing, singular is fine. Microsoft's own example is /orders/99/customer, because an order has exactly one customer. Google also says not to invent fake plurals: if a word has no plural, like "info," keep the singular.

Hyphen, underscore or camelCase in URLs?

Use lowercase words separated by hyphens in the path: /order-items, not /orderItems or /order_items. Zalando's rule 129 requires kebab-case path segments matching ^[a-z][a-z\-0-9]*$. Google Search Central recommends hyphens over underscores in URLs. And RFC 3986 makes paths case-sensitive, so mixed case invites duplicate URLs.

That last point is the one that bites in production. RFC 3986 says only the scheme and host are case-insensitive. Everything else is "assumed to be case-sensitive." So /Order-Items and /order-items can be different resources, and somebody will type the wrong one.

Be aware the big guides do not fully agree:

GuidePath segmentsQuery parameters
Zalandokebab-case (/sales-orders)snake_case (?sort_by=)
Google AIPscamelCase collection IDs (/userEvents)snake_case field names
Google Search Centralhyphens over underscorestrim unnecessary params

My recommendation: kebab-case paths, and query parameter names that match the casing of your JSON fields. If your JSON says created_at, filter with ?created_at=. Consistency inside one API matters more than which guide you follow.

Path parameters vs query parameters: when do you use each?

Use a path parameter when the value identifies a resource, and a query parameter when it narrows, sorts, pages or shapes a list. /orders/42 names one order, so 42 is in the path. /orders?status=shipped returns the orders that match, so status is a query parameter. A useful test: if removing the value breaks the URL's meaning, it belongs in the path.

QuestionPath parameterQuery parameter
Does it identify one resource?Yes: /users/7No
Is it optional?No, it is part of the addressYes, with a sensible default
Can you combine several?AwkwardEasy: ?status=paid&country=de
Typical useIDs, slugs, parent IDsfilters, sort, fields, limit, cursor

One cost to keep in mind: Microsoft notes that query parameters "form part of the resource identifier that many cache implementations use as the key to cached data." So ?sort=price and ?sort=-price are two cache entries. That is fine, just know it.

REST API nested resources: how deep is too deep?

Nest one level, two at most. Microsoft's guide says to avoid URIs "more complex than collection/item/collection," like /customers/1/orders. Zalando's rule 147 says to use 3 or fewer nesting levels. Past that, URLs get long, and every hop is a relationship you can never change without breaking clients.

Nesting makes sense when the child cannot exist without the parent, or when the parent scopes it. /customers/1/orders reads naturally: "the orders belonging to customer 1."

It stops making sense when the child has its own global ID. Microsoft's example: instead of /customers/1/orders/99/products, use /customers/1/orders to find the order, then /orders/99/products. Order 99 is unique on its own, so there is no reason to drag the customer along.

Two small rules from Google's AIP-122 are worth stealing:

  • If a child collection repeats the parent's name, drop the prefix: /users/7/events, not /users/7/userEvents.
  • Never repeat the same collection name in one path, like /people/1/people/2.

How do you design RESTful search and filtering?

Put filters in the query string on the collection itself, like GET /orders?minCost=100&status=shipped, which follows Microsoft's own example. Use conventional names for the rest: Zalando lists q for free-text search, sort, fields, limit, offset and cursor. Do not invent /orders/search/shipped, because search results are just a filtered view of the collection.

Here is a full search URL built that way:

GET /products?q=keyboard&category=electronics&min_price=50&sort=-price&fields=id,name,price&limit=20

Three styles show up in real APIs. Pick one per API:

  1. Simple key-value. ?status=shipped&country=de. Each field is its own parameter. Easiest to cache and document. Start here.
  2. Range parameters. ?min_price=50&max_price=200. Covers most needs beyond plain equality without a parser.
  3. A filter expression. Google's AIP-160 says a list request should have "exactly one filtering field, string filter," with expressions like a.b > 42. Powerful, but you now own a small query language and its parser.

For sorting, Zalando's sort takes a comma-separated field list. A leading - for descending is a common convention.

What are REST API pagination best practices?

Paginate every list endpoint from day one, cap the page size, and prefer cursors over page numbers on data that changes. Google's AIP-158 says adding pagination to an existing method later is a "backwards-incompatible change." Stripe caps limit at 100 with a default of 10. GitHub caps per_page at 100 on most endpoints.

Why cursors? Zalando's rule 160 explains it: offset pagination "may create duplicates or lead to missing entries if rows are inserted or deleted" between requests. If a new order lands while you read page 2, the last row of page 2 shifts onto page 3 and you see it twice. A cursor points at a row, not a position, so nothing shifts.

StyleExampleGood forWeakness
Offset?limit=25&offset=50Small, stable tables, "jump to page 5" UIsDuplicates and gaps when data changes; slow on huge tables
Cursor (opaque token)?limit=25&cursor=eyJpZCI6...Feeds, large or fast-changing dataCannot jump to an arbitrary page
Cursor (object ID)Stripe: ?limit=3&starting_after=cus_4QFJTime-ordered listsTies you to one sort order

The details that separate a good paginated API from a fragile one:

  1. Give a default and a maximum. Microsoft suggests defaults like limit=25&offset=0 and an upper limit to protect the service. Google's example coerces anything above 1000 down to 1000.
  2. Make cursors opaque. Google's AIP-158 says tokens must be URL-safe and not user-parseable, and that base64 alone is not enough.
  3. Signal the end clearly. Stripe returns has_more. Google returns an empty next_page_token. GitHub sends a Link header with rel="next", rel="prev", rel="first" and rel="last".
  4. Re-check auth on every page. AIP-158 says a page token can never grant authorization.

How do you delete multiple records in REST?

For a handful of IDs, call DELETE /orders/{id} once per record. For real bulk deletes, send a POST to a batch endpoint with the IDs in the body, like Google's POST /orders:batchDelete. Avoid putting a body on DELETE: RFC 9110 says DELETE content "has no generally defined semantics" and some implementations may reject the request.

That RFC line is the whole reason the "DELETE with a JSON body" approach is risky. RFC 9110 names request smuggling as the concern. Your framework may accept the body, but a proxy or load balancer in between may not.

Google's AIP-235 is the cleanest published pattern:

  • The method must be POST, not DELETE.
  • The URL must end in :batchDelete.
  • The body carries a names field listing what to delete.
  • A synchronous batch delete must be atomic: all or nothing.

If you allow partial success instead, Zalando's rule 152 says batch requests respond with 207 Multi-Status and a per-item result, so the client can see which records failed. For a different take on status codes in internal APIs, read why your private API may not need HTTP status codes.

When should you break these rules?

Break them when the operation is not really a resource. Microsoft's guide admits some operations, like a calculator's add, do not map to a noun, and shows /add?operand1=99&operand2=1 as an acceptable "pseudo resource." Google's AIPs use a :verb suffix, like :batchDelete, for the same job. Reach for these only after a noun has failed.

Do not force these rules when you are not building REST at all. If your clients and server ship together and you call functions rather than fetch resources, an RPC style may fit better. We compared the options in REST vs gRPC: when to use each.

And do not bend them because the database looks a certain way. Microsoft warns against APIs that "mirror the internal structure of a database." Your URL is a public contract. Your tables are not.

A small example: one resource, every rule

Here is an Express router that applies all of it to an orders API. It is short on purpose.

import express from "express";
const router = express.Router();

// Collection: plural, lowercase, hyphenated
router.get("/orders", async (req, res) => {
  const limit = Math.min(Number(req.query.limit) || 25, 100); // default 25, cap 100
  const { status, min_total, sort = "-created_at", cursor } = req.query;

  const { items, nextCursor } = await db.orders.list({
    status, minTotal: min_total, sort, after: decodeCursor(cursor), limit,
  });

  res.json({
    data: items,
    next_cursor: nextCursor ? encodeCursor(nextCursor) : null, // null = last page
  });
});

// One resource: ID in the path
router.get("/orders/:orderId", getOrder);

// One level of nesting: line items only exist inside an order
router.get("/orders/:orderId/line-items", listLineItems);

// Bulk delete: POST with IDs in the body, never DELETE with a body.
// The colon is escaped because Express reads ":" as a route parameter.
router.post("/orders\\:batchDelete", async (req, res) => {
  const ids = req.body.ids ?? [];
  if (ids.length > 1000) return res.status(400).json({ error: "max 1000 ids" });
  await db.transaction((tx) => tx.orders.deleteMany(ids)); // atomic
  res.status(204).end();
});

And the URLs a client would call:

GET  /orders?status=shipped&min_total=100&sort=-created_at&limit=25
GET  /orders?limit=25&cursor=b3JkXzQyMQ
GET  /orders/ord_421
GET  /orders/ord_421/line-items
POST /orders:batchDelete      {"ids": ["ord_1", "ord_2"]}

Read the client list top to bottom and you can see the library again: shelves and books in the path, instructions to the librarian in the query, and one clearly labelled request for the bulk job.

FAQ

Should REST API URLs use camelCase?

Not in the path. Zalando requires kebab-case path segments, and RFC 3986 makes paths case-sensitive, so lowercase hyphenated paths avoid duplicate URLs. Google's AIPs do use camelCase collection IDs, so you will see it in the wild. For query parameters, match the casing of your JSON fields.

RFC 9110 says content in a GET request "has no generally defined semantics" and may cause some implementations to reject the request. Keep searches in the query string whenever they fit. When a search is too complex for a URL, a POST to a dedicated search endpoint is the safer choice.

Is offset pagination ever fine?

Yes, for small tables that rarely change and for interfaces that need "jump to page 7." Zalando notes that offset pagination is more familiar and better supported by frameworks. Switch to cursors once rows are being inserted or deleted while users page through results.

Should the API version go in the URL?

Microsoft's guide covers URI versioning (/v2/customers), query string versioning (?version=2) and header versioning, and notes that URI and query string versions are cache-friendly. A /v1/ prefix is easy to see and easy to route. Whichever you pick, keep it out of individual resource names.

Take one endpoint you shipped this month, compare it to the before-and-after table above, and fix the first rule it breaks. Then browse the free tools on Art of Code, and read about the system design mistakes beginners make.

Written by Adithya Guttha, Founder of Art of Code.

#rest api#api design#url design#pagination#backend

Sign in to like and save posts.

Curious Adithya

Written by

Curious Adithya

@curious_adithya · 5 followers

Discussion

Comments (0)

Want to join the conversation?

Sign in to comment

No comments yet. Be the first to comment!