# Resources and URLs — Routebase

> How to pick the things your API exposes, why nesting stops paying after one level, what a path convention is worth, and which identifiers are safe to put in a URL.

Canonical page: https://routebase.dev/guides/api-design/resources-and-urls/
Chapter 1 of 10 · API Design · Last reviewed 2026-09-13 · The Routebase Team

The resource model is the first decision and the most expensive one to revisit, because every URL you publish inherits it. Everything else in this guide sits on top of the nouns you pick here.

## Model the domain, not the procedure

The common failure is exposing an implementation as an endpoint. Something like `POST /runSyncJob?mode=full` describes how your system works internally, and it breaks the moment the internals change.

The alternative is to name the thing rather than the action, and let the method carry the verb. A sync becomes a resource that can be created, read and cancelled, which means a caller who knows how to create one already knows how to read it. The [API design principles](/blog/api-design-principles/) post covers that reframing with worked examples.

What the short version leaves out is where the rule stops. Some operations genuinely have no noun, and forcing one produces worse names than the verb you were avoiding. Those cases get their own chapter on [actions that are not CRUD](/guides/api-design/actions-that-are-not-crud/), because the answer there is a trade-off rather than a principle.

## Collections and items

Almost every resource shows up twice, once as a collection and once as an item inside it, and the two behave differently enough to be worth separating.

A collection accepts a create and answers a list, so it needs pagination, filtering and a sort order from the start. An item accepts a read, a replace, a partial update and a delete, and it needs an identifier that stays valid. Callers assume that the item under a collection has the same shape as the entries the collection returns, and breaking that assumption is a surprise you will explain repeatedly.

The one place teams disagree is whether the collection response wraps its items in an envelope. An envelope costs one level of indirection and buys somewhere to put paging information, which is why most APIs that page at all end up with one.

## Nesting expresses ownership, once

Nesting reads well at one level, because the path then states a fact about ownership that a caller can rely on.

_Figure: One level of nesting states that the child belongs to the parent, and the caller needs one identifier it already has. At three levels the path has become a traversal, so the caller has to carry two identifiers it never asked for, and every one of them is now part of your contract. When the leaf has an identity of its own, the flat address needs a single identifier and survives a reparenting that would break the nested form._

The failure is gradual rather than sudden. Each additional level looks reasonable on its own. The cost only shows up when a caller holds a leaf identifier and has to reconstruct the ancestors to build a URL. At that point clients start caching identifiers they should never have known, and moving a record to a different parent becomes a breaking change.

A useful test is whether the leaf can be found without its ancestors. If a payment can be looked up by its own identifier alone, it deserves a top-level collection and a reference to the invoice it belongs to. If it genuinely cannot exist outside its parent, one level of nesting is telling the truth.

## Naming the segments

Three naming decisions repeat in every API. All three are conventions rather than requirements.

The first is plural against singular for collections, where plural is the more common choice. The second is the case of multi-word segments, where kebab-case is common and lowercase avoids case-sensitivity bugs between servers. The third is whether the same concept keeps the same word everywhere, which is the only one of the three with an obviously wrong answer.

Because none of these is a standard, an existing API has no correctness argument for migrating. It has a consistency argument. That argument is usually weaker than the cost of breaking every published URL. The realistic move on an existing API is to fix the convention going forward and enforce it on new paths.

## Identifiers are part of the contract

The moment an identifier appears in a URL, it stops being an implementation detail. A caller will store it, log it, and put it in a support ticket eighteen months from now.

That has two consequences worth deciding up front. A sequential key exposes your record count and makes neighbouring records guessable, so most APIs expose an opaque identifier instead. And whichever you expose, you can no longer change the underlying key without breaking clients, which is a constraint on the database rather than on the API.

Some resources also have a natural key, such as an order number that already exists in the business. Accepting both that key and an internal identifier at the same path is convenient and ambiguous, so it is worth choosing one and offering the other as a filter on the collection.

## In Routebase

Endpoints live in a folder tree rather than a flat list of tag groups, and that tree is the same structure readers see in the published reference documentation.

Folders and tags do different jobs on purpose. Folders define the visible hierarchy, while tags are flat labels that cut across it, and a tag carries a description that is exported as the OpenAPI tag description. Because the two can drift apart, renaming a folder asks what should happen to the tags, and it tells you which choice changes the exported contract and which does not.

Path variables are read out of the path itself. Creating `GET /users/{id}` also creates the path parameter, and removing it from the path removes the parameter, with a dialog first when the parameter carries a description or an example you would lose.

The path convention is a style guide rule rather than a fixed opinion. The rule for URL paths defaults to kebab-case and also accepts camelCase, PascalCase and lowercase, chosen from a dropdown in the rule table. So a team that has always written `/userProfiles` can keep the check without changing its URL scheme. Uniqueness is checked at a level that matches how OpenAPI reads paths, since `/items/{id}` and `/items/{itemId}` address the same route and are reported as a collision. The [Endpoints guide](https://docs.routebase.dev/endpoints/) covers the tree and the tag behaviour, and the [Style Guide](https://docs.routebase.dev/style-guide/) covers the naming rules and their severities.

## Frequently asked questions

### Should REST resources be plural or singular?

Plural for collections is the common convention, so a list is at the collection path and one item sits under it with an identifier. Neither form is required by any specification, and both are readable. What matters is that you use the same form for every collection in the API, because a caller who has learned one path is trying to guess the next one.

### How deep should a REST URL be nested?

One level of nesting expresses ownership and stays readable. Beyond that the path starts encoding a traversal rather than an address, and callers end up holding identifiers they should never have needed. When a nested resource has an identity of its own, give it a top-level collection and let it reference its parent by identifier instead.

### Should API URLs use kebab-case or camelCase?

Kebab-case is the more common choice for path segments, and lowercase paths avoid an entire class of case-sensitivity bugs. Neither is a standard, so an existing API with camelCase paths has no correctness reason to migrate. The decision worth enforcing is that one convention applies to every path in the API.

### Should database IDs be exposed in API URLs?

A sequential database key in a URL tells a caller how many records you have and makes neighbouring records guessable, so most teams expose an opaque identifier instead. The identifier also becomes part of the contract the moment it appears in a URL, which means the internal key can no longer be changed freely. Both of those are reasons to decide the identifier deliberately rather than inheriting it.

---

[Routebase](https://routebase.dev/) — [Sign up](https://app.routebase.dev/): Every account starts with a 14-day Pro trial — no credit card required.
