DEV Community

Cover image for REST API Design Best Practices for Sub and Nested Resources
K for moesif

Posted on • Originally published at moesif.com

REST API Design Best Practices for Sub and Nested Resources

Cover image by Marco Verch Professional Photographer and Speaker, on Flickr

Many questions arise when we start designing an API, especially if we want to create a REST API and adhere to the REST core principles:

  • Client-Server Architecture
  • Statelessness
  • Cacheability
  • Layered System
  • Uniform Interface

One topic in this space that is debated quite often is the nesting of resources also called sub-resources.

  • Why would anyone nest their resources?
  • Are they a good idea in the first place?
  • Should we nest our resources?
  • When should we nest our resources?
  • If we nest our resources, what should we keep in mind?

Since this decision can have a considerable impact on many parts of your API, like security, maintainability or changeability, I want to shine some light on this topic in hopes that it helps to make this decision more educated.

First, we will look into the reasons that speak for nested resources. After that, we will talk about the reasons that make nested resources problematic.

Why

Let's start with the central question: Why should we use a nested resource design approach?

Why should we use this approach:

/posts/:postId/comments/:commentId
/users/:userName/articles/:articleId
Enter fullscreen mode Exit fullscreen mode

over this one:

/comments/:commentId
/articles/:articleId
Enter fullscreen mode Exit fullscreen mode

The main reason for this approach is readability; a nested resource URL can convey that one resource belongs to another one. It gives the appearance of a hierarchical relationship, like directories give in file-systems.

These URLs convey less meaning about the relationship:

/books/:bookId
/rating/:ratingId
Enter fullscreen mode Exit fullscreen mode

Than these URLs:

/books/:bookId
/books/:bookId/ratings/:ratingId
Enter fullscreen mode Exit fullscreen mode

We can directly see that the rating we are requesting belongs to a specific book. In many cases, this can make debugging easier.

I said appearance of hierarchical relationship because the underlying data-model doesn't have to be hierarchical. For example, on GitHub, a user can have contributed code to multiple repositories, and a repository can have contributions from various users. It's a many-to-many relationship.

/users/:userName/repos
/repos/:repoName/users
Enter fullscreen mode Exit fullscreen mode

If you only knew about one of the endpoints, it would seem that it was a one-to-many relationship.

Other, more technical reasons, are relative IDs or context of the nested resource.

Houses, for example, have house-numbers, but they are local to the streets they belong to. If you know the house has number 42, but you don't remember the street it doesn't help you much.

/street/:streetName/house/:houseNumber
Enter fullscreen mode Exit fullscreen mode

Another example could be file-names in a file-system. Just knowing that our file is called README.md won't help if there are hundreds of files named like that in hundreds of different directories.

/home/kay/Development/project-a/README.md
/home/kay/Development/project-b/README.md
Enter fullscreen mode Exit fullscreen mode

If we use a relational database, we often have unique keys for all of our data records, but as we see, with other kinds of data-stores, like file-systems, this doesn't necessarily have to be the case.

Nested URLs can also be manipulated rather easily. If a hierarchy is encoded in an URL we can drop parts of the URL to climb this hierarchy up. This makes APIs with nested resources quite a bit simpler to navigate.

To sum it all up, we want to use nested resources to improve readability and in turn developer experience and sometimes we even have to use them because the data-source doesn't give us a way to identify a nested resource solely by their ID.

Why Not

Now that we talked about the reasons why we should use nesting, it's also important to talk about the other side: Why should we not nest our resources?

While nesting is sometimes necessary and can't be avoided, it is often a choice that comes with specific costs or dangers we should keep in mind.

Let's look at them one-by-one.

Potentially Long URLs

We learned before that nesting resources could make our URLs more readable, but this isn't a sure bet.

Especially in rather complex systems with many relationships between the resources the nested approach can lead to rather long and complicated URLs.

/customers/:customerId/projects/:projectId/orders/:orderId/lines/:lineId
Enter fullscreen mode Exit fullscreen mode

This issue can become even more problematic if we use long strings as IDs:

/customers/8a007b15-1f39-45cd-afaf-fa6177ed1c3b/projects/b3f022a4-2970-4840-b9bb-3d14709c9d2a/orders/af6c1308-613f-40ff-9133-a6b993249c88/lines/3f16dca9-870e-4692-be2a-ea6d883b9dfd
Enter fullscreen mode Exit fullscreen mode

So when we start to go down this path, we should step back sometimes and look if we are still accomplishing our goal of improved readability.

A rule of thumb is a maximum nesting depth of two. Sometimes a depth of three is also okay. For example, if our IDs are short and easily readable.

/author/kay-ploesser/book/react-from-zero/review/23
Enter fullscreen mode Exit fullscreen mode

_What is Moesif? Moesif is the most advanced API analytics service used by over 2000 organizations to understand how your customers use your APIs and which resources they use the most.

Redundant Endpoints

In general, using nested resources isn't as flexible as using root resources only.

For example, if we have a many-to-many relationship. Repositories have multiple contributors, but every user can also contribute to various repositories.

If we want to realize this with nested resources, we have to create two endpoints alone for this relationship

/user/:userName/repositories
/repositories/:repositoryName/contributors
Enter fullscreen mode Exit fullscreen mode

If we want to realize this without nesting, we could define one root resource for contributions that also allows filter parameters in its URL.

/contributions?userName=:userName&repositoryName=:repositoryName
Enter fullscreen mode Exit fullscreen mode

The parameters are optional, so we could also use it to get all contributions, and we can PUT and POST to it to change and create relationships.

While this doesn't seem to be a problem with one-to-many relationships, in which one part of the relationship can't have multiple connections, we can still get at a point where we want to search for all records of a nested resource across its parent resources.

So while having this endpoint:

/mothers/:motherName/children
Enter fullscreen mode Exit fullscreen mode

We could still want to get all children of all mothers and create a new endpoint for this

/children
Enter fullscreen mode Exit fullscreen mode

Redundant endpoints also increase the surface of our API, and while more readable URLs for our resource relationships are a good thing for developer experience, a giant amount of endpoints is not.

Multiple endpoints increase the effort for the API owner to document the whole thing and make onboarding for new customers much more troublesome.

Multiple endpoints that return the same representations can also lead to problems with caching and can violate one of the core principles of RESTful API design.

This problem can be solved via HTTP redirects, so all representations are returned from a central root resource and can be cached, but there is still code needed to implement this.

It can also violate another core principle, the Uniform Interface.

When a client holds a representation of a resource, including any metadata attached, it has enough information to modify or delete the resource on the server, provided it has permission to do so.

If the representation doesn't include information about the nesting and we don't have root resources to directly access it; we can't create, update or delete it.

Multiple Database Queries

If we traverse a relationship graph down instead of using one unique identifier (if it exists) to retrieve a representation from a resource, we need to check if the relationship realized in an URL holds true.

Take this example of getting a nested comment

/blogs/X/articles/Y/comments/Z
Enter fullscreen mode Exit fullscreen mode
  • Is there a blog with ID X?
    • Let's ask the DB!
  • Does our blog with ID X have an article with ID Y?
    • Let's ask the DB!
  • Does our article with ID Y have a comment with ID Z?
    • Let's ask the DB!

Getting all comments on all articles of all blogs is also a problem.

  1. query for all blogs
  2. query each blog for each of its articles
  3. query each article for each of their comments

The N+1 Query problem hit's us hard with this API design.

If we just had a root resource for our comments, we could query it and throw in a few filter parameters if needed. If comments have globally unique IDs, we could query them directly.

/comments/Z
/comments?before=A&after=B
Enter fullscreen mode Exit fullscreen mode

Security

If we share links to our resources, all data encoded inside the URL is potentially exposed to third parties, even if they don't have access to request the representation from our API.

URLs will be logged by intermediates when requesting anything via HTTP on the Internet, so the links don't even have to be actively shared on social media or the like.

For example, this image link:

/users/:userName/images/:imageId
Enter fullscreen mode Exit fullscreen mode

If we share it somewhere, we people learn that we have a user with a specific name and that they uploaded images on our service.

If the image link was a root resource, no such information would be apparent.

/images/:imageId
Enter fullscreen mode Exit fullscreen mode

Changing URLs

If our relationships change, the URLs they're encoded into aren't stable anymore.

Sometimes this can be useful, but more often than not we want to keep our URLs so old links won't stop working.

For example, this owner-product relationship:

/owners/kay/products/1234
/owners/xing/products/1234
Enter fullscreen mode Exit fullscreen mode

If the product were accessible as a root resource it wouldn't matter who owns it.

/products/1234
Enter fullscreen mode Exit fullscreen mode

As I mentioned before, if the relationships change rather often, we can also consider to treat the relationship itself as a resource.

/posessions?owner=kay&product=1234
Enter fullscreen mode Exit fullscreen mode

With this approach, we can change the relationships via one single endpoint but link our other resources directly via their own root resource that isn't affected by this change.

Wrap Up

So what is the takeaway of all this?

Should we nest our resources or not?

Sometimes it can't be avoided, because the data-source simply doesn't give us any other choice, but if we have the choice we should consider all the pros and cons.

If the data is strictly hierarchical, not too deploy nested and the relationships don't change too often, I would go with nested resources.

The downsides aren't too big for the wins in developer experience.

If the data is prone to relationship changes or has quite complex relationships to start with, it's easier to maintain root resources or even to consider completely different approaches like GraphQL.

More endpoints and, as the nesting scenario implies, more complex endpoints means more code and documentation to write. This doesn't lead to a question of feasibility in terms of skills or know-how, but often simply questions of development and maintenance costs. So even if we know how to do it and security or cacheability isn't much of a concern, we have to ask ourselves if it gives us any competitive advantage.


Moesif is the most advanced API Analytics platform, supporting REST, GraphQL and more. Over 2000 organizations use Moesif to track what their most loyal customers do with their APIs. Learn More


Originally published at www.moesif.com

Oldest comments (6)

Collapse
 
jsardev profile image
Jakub Sarnowski

Thanks for the article! :)

I was creating an API just by myself for the first time recently and I stumbled upon this problem.

I had a files table, and a bunch of entities like reports, comments which had a relationship to files. Now, there would not be a problem with a root /files endpoint, but unfortunately, the permission to the files was only granted if you had permission to its parent (in this case a Report, Comment etc.). So I've created a /comments/:commentId/files/:fileId. And then a similar one for Reports. And everything was cool, but...

Later on, another entity needed a relationship to files. And another one. So I had to create another */files/:fileId endpoints, which would've been almost identical to the other ones (it just checked for different permissions).

This felt totally wrong, those endpoints were a redundant duplication. So I've refactored it to have just a root /files/:fileId endpoint. The only downside of this was that I had to determine the File parent, so I needed to check all of the potential parent tables. But aside from that - it was a lot more readable and pleasant to use!

What would be your approach to this problem?

Collapse
 
kayis profile image
K

Glad you liked the article! :D

About your problem.

I have no idea how your resources are linked together, but from what you are saying you're fighting a bit with redundancy.

One possible solution could be the creation of a root resource for files that handles all CRUD actions and then add nested resources for read actions only that just respond with redirects to the corresponding files resource.

That way you could read /comments/1235/files when needed and only had to implement something like /files?comment=1235 that is the target of the redirect.

Collapse
 
jsardev profile image
Jakub Sarnowski

You're right, it's hard to say without the whole context :) But I won't describe the whole thing here as I don't think it's the right place to discuss complicated problems :D

Anyways, your article gave me some insight on the problem and some ideas on different approaches with nested resources :) Also thanks for the suggestion in your comment as I didn't think about this kind of solution!

Thread Thread
 
kayis profile image
K

You are welcome :)

Collapse
 
steelwolf180 profile image
Max Ong Zong Bao

Hmmm... I think having nested url of 3 depth can be challenging and documentation can be a pain so I usually stick with only 2.

But I do agree that having nested url has it's place for certain reasons.

Collapse
 
kayis profile image
K

Yes, I think 2 would be a good rule of thumb, but when your data forms a tree it's okay to go deeper.