Qubyte Codes

Routes of RESTful services with nested resources

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 tradeoffs 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 that 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!