Routes of RESTful services with nested resources
Published
Nested resources are common in real life, and when you're a programmer working on RESTful APIs you may have noticed that there are often trade-offs when it comes to formatting the routes of nested resources.
Let's say we're building an API for carpenters. Their guild wants a way to help the carpenters manage their clients, and the items which they're creating for their clients (jobs). A set of routes for this might look like
GET /carpenters
POST /carpenters
GET /carpenters/:carpenterId
GET /clients
POST /clients
GET /clients/:clientId
GET /jobs
POST /jobs
GET /jobs/:jobId
where segments preceded by a colon are placeholders for a UUID. The first route of each group gets a collection, the second creates a new member, and the third gets a member by its ID. There may well be other operations like the third which act on individual members of a collection, such as PUT for replacing and DELETE for removal.
The problem with this layout is that, as a carpenter, you're rarely going to want to see clients which aren't your clients. There may also be lots of jobs, and making a request for a list of jobs could select a huge number of them. Usually you'll want to filter it by the client. We can use query parameters to supply additional context, but this seems a bit clumsy. Let's have another go
GET /carpenters
POST /carpenters
GET /carpenters/:carpenterId
GET /carpenters/:carpenterId/clients
POST /carpenters/:carpenterId/clients
GET /carpenters/:carpenterId/clients/:clientId
GET /carpenters/:carpenterId/clients/:clientId/jobs
POST /carpenters/:carpenterId/clients/:clientId/jobs
GET /carpenters/:carpenterId/clients/:clientId/jobs/:jobId
It's pretty clear now that these are nested resources. It's no longer possible to select a collection without refining it by its parent resources.
The problem now is that it's a bit annoying to have to use all the parent IDs of a list or a member. I recently realised (and I'm probably late to the party) that a good solution is a compromise between the two
GET /carpenters
POST /carpenters
GET /carpenters/:carpenterId
GET /carpenters/:carpenterId/clients
POST /carpenters/:carpenterId/clients
GET /clients/:clientId
GET /clients/:clientId/jobs
POST /clients/:clientId/jobs
GET /jobs/:jobId
In this arrangement, we need to use a route with a parent ID when listing a resource or creating a new member. When we already have an ID of a resource we don't need to include its parent in the path. This includes other singular operations such as PUT and DELETE.
Finally, a word on nesting. The implicit assertion made above is that a child resource has a single parent, which may not be the case. Think of the solution above as a filtering mechanism. i.e. a client may have contracted more than one carpenter. You may also want to list jobs for a carpenter, rather than for a client. If you find yourself writing too many routes, using query parameters may be the lesser evil. Be pragmatic!