Structuring RESTful URLs
If you haven't been following my blog and just stumbled upon this post, this is the 6th article on my series about designing REST APIs, you can read the rest here. If, on the other hand you've been following and you like what you've read or resonate with my opinions, consider subscribing so you don't miss anything.
Today we are talking about an essential part of REST, designing good URLs for our resources.
In a RESTful system, URLs are not just "technical details" that can be overlooked. They are part of the API's public interface and play a crucial role in how easily clients can understand and use it. A well designed URL communicates meaning without requiring additional documentation.
A RESTful URL should represent a resource and its relationship to other resources. It should avoid embedding actions or workflows and focus on expressing structure instead, REST offers other ways to express actions and workflow we will talk about them later on.
URLs as identifiers, not instructions
A common anti pattern in API design, especially in REST, is to treat URLs as remote function or procedure calls. Endpoints such as /getUserDetails or /deleteBook may work functionally, but they describe actions rather than resources, moreover, they tie the action and the resource together, resulting in way more URLs than we need.
In REST, the URL identifies what is being accessed, while the HTTP method defines what is being done. For example:
GET /books/VE101
DELETE /books/VE101
Both requests target the same resource. The difference in behavior comes entirely from the HTTP method. This separation improves clarity and predictability while also decoupling the resource from the action.
Hierarchies and relationships
Resources often have natural relationships. A book belongs to an author. A waiting list belongs to a book, for example. RESTful URLs should reflect these relationships when they are meaningful so that the API consumers can reason about them and use our API more efficiently.
For example:
GET /authors/13258/books
This reads as retrieving the books associated with a specific author. The structure mirrors the mental model of the domain and avoids the need for complex filtering using GET parameters on the book resource.
The same result could be achieved by:
GET /books?author_id=13258
But, if this is a common query, it's convenient and more readable to have an endpoint for retrieving an author's books from the author resource. It's more intuitive and reflects better how the API clients are using our API.
Subresources
Subresources should be used when the child resource does not make sense without its parent. A waiting list, for example, doesn't make any sense without the resource it refers to, we might know it's a waiting list, but what are we waiting for?:
GET /books/VE101/waiting-list
This structure clearly expresses dependency and context.
We can think of this the way we think and design URLs in REST as a directory structure in a file system, where we have folders and files as well as sub-folders holding resources we might be interested in.
Query parameters and filtering
Query parameters are best used for filtering, sorting, and pagination. They should not be used to identify resources uniquely.
For example, filtering books by publication year can be expressed as:
GET /books?publish_year=1939
However, retrieving a specific book by identifier should not rely on query parameters:
GET /books/VE101
More often than not, we end up combining both approaches. A well structured API uses path segments for identifying resources and query parameters for refinement.
Versioning and stability
URLs are part of the API contract and should change rarely. When breaking changes are necessary, versioning helps preserve compatibility given the clients consume the right version of the resource.
A common convention is to include the major version at the beginning of the path:
/v1/books
This makes breaking changes explicit and allows clients to migrate at their own pace.
While versioning introduces complexity, it is often preferable to silently breaking existing clients.
I have a very strong opinion against this approach of embedding the version into the URL, often what changes is not the resource, but rather the payload. On the other hand, we might be introducing changes in maybe 1 or 2 resources, adding a version prefix to the API would upgrade all resources, even the ones that were untouched. This is why I prefer to use request headers to indicate which version should be sent instead of defining an entirely new set of URLs.
Readability and consistency
RESTful URLs should be readable and consistent. Using lowercase characters, avoiding file extensions, and sticking to a single naming convention improves usability.
Long or deeply nested URLs should be avoided not because of technical limits, but because they become harder to understand and maintain. Two or three levels of nesting are usually sufficient to express meaningful relationships.
A well designed URL should be easy to read and easy to reason about.
Don't forget to subscribe to the mailing list if you liked what you read so far or resonate with my opinions.