1. Resource relationships and parent context in URI
Nesting in URI is needed so the path immediately shows the resource context. As soon as something more complicated than "one table with everything in it" appears in your system, you run into relationships: a task can have comments, attachments, and tags. And all of that should be exposed through the API so the client does not have to guess what belongs to what. This is exactly where nesting is needed: when the path contains the parent identifier, it directly says "this is part of that".
Think of a URI as an address on an envelope. There is the "city" /tasks, there is the "house" of a specific task /tasks/{taskId}, and inside this "apartment" there can already live "comments" and "attachments". Yes, it is a down-to-earth analogy, but it shows the goal well: nesting is not decoration, but a way to embed context directly into the address so that it is understandable without extra documentation or guesswork.
In the Task Tracker API, this is exactly our case: a comment without a task is "a comment about what?", an attachment without a task is "attached where?". The question is not whether it is technically possible to do GET /comments/{commentId} — it is. The question is whether that would be clean and predictable from a contract perspective for a client that lives with your API for months or years.
Nested resources and path depth
So we do not talk in vague terms, let us fix the terminology. A nested resource is a resource addressed in the context of another resource: the URI explicitly contains the parent identifier. In our project, examples include /tasks/{taskId}/comments and /tasks/{taskId}/attachments. And "path depth" is simply the number of nesting levels: how many "steps" you take from the base path to the target entity.
Depth matters because it directly affects readability and API fragility. A small amount of nesting usually helps, but too much turns a URI into a matryoshka doll that nobody wants to open: long, inconvenient, and every change in the relationship model starts breaking addresses. Good nesting answers the question "which context is mandatory", while bad nesting answers "which relationships I found in the database and decided to reflect in the address".
Let us look at a very simple template that we will use as a mental "ruler":
// Base path for the task collection (top-level API)
String tasks = "/api/v1/tasks";
// Path to a specific task: context is "we work inside one task"
String task = "/api/v1/tasks/{taskId}";
// Subresource: comments live in the context of a specific task
String taskComments = "/api/v1/tasks/{taskId}/comments";
Here you can see the path "growing" logically: first the collection, then the specific resource, then its subresource. The key is that each added segment must actually add meaning, not just length.
2. Criteria for useful nesting
The usefulness of nesting is not defined by a short rule, but by how the resource lives. Usually a beginner wants a "one-paragraph rule", but that does not work here: the decision depends on how the resource is structured and how it is used. So it is better not to think in terms of "nested or not nested", but to ask yourself a few questions that quickly clarify the picture. Importantly, these questions are not about Spring or databases — they are about the contract and the meaning.
Below is a reference table. Do not treat it as a law of nature; it is more of a set of healthy signals.
| Question for the resource | If the answer is "yes" — nesting is usually appropriate | If the answer is "no" — nesting is often unnecessary |
|---|---|---|
| Does the resource exist only in the parent context? | /tasks/{taskId}/comments looks natural | the resource can be made top-level |
| Does the client almost always access it "through the parent"? | nesting makes calls predictable | a direct path is better |
| Is the resource a supporting subresource rather than an "equal entity"? | comments and attachments are classic candidates | a separate entity often belongs at the top level |
| Do you need the URI to immediately show who the object belongs to? | nesting is the most honest way | ownership can be expressed differently, but it will be less explicit |
| Do you want to avoid "orphaned" objects with unclear context? | nesting helps keep the model within bounds | top-level resources will need extra rules to compensate |
Now let us apply this to our entities. A comment in the course is a supporting subresource of a task: it is created "for a task", read "for a task", and deleted "for a task". The same is true for an attachment: it does not exist as an independent "file resource of the world", but as an attachment of a specific task.
This gives us a practical and clear URI:
// Comments of a specific task: a subresource, without taskId it loses meaning
String taskComments = "/api/v1/tasks/{taskId}/comments";
// Attachments of a specific task: also a subresource at the same context level
String taskAttachments = "/api/v1/tasks/{taskId}/attachments";
The meaning is instantly clear: "comments of a specific task" and "attachments of a specific task". The client does not need to know the internal data model — it does not leak outward, because the URI already explains everything.
3. Signs of a fragile hierarchy
A fragile hierarchy begins where the path grows faster than the meaning. Nesting is a useful tool, but it has a favorite hobby: spreading quietly. Today you nested /tasks/{taskId}/comments, tomorrow you wanted "comment attachments", the day after tomorrow "attachment ratings", and now you have a URI as long as the variable name of a junior Java developer who is trying to be as clear as possible and accidentally invents a new language.
The main sign of a fragile hierarchy is when the path becomes deep and you can no longer honestly explain why each level is there. A classic example of "over-nesting":
// Too deep a path: the contract promises extra relationships that are hard to change later
String tooDeep = "/api/v1/tasks/{taskId}/comments/{commentId}/attachments/{attachmentId}";
The problem is not even that the string is long, although that is also true. The problem is that you are baking a hypothesis into the contract: "an attachment is part of a comment". But if tomorrow you decide that attachments belong to the task as a whole (which is the canonical model for our course), the whole world of clients will suffer because of your yesterday's romantic decisions.
There is also a subtler sign: you nest a resource under a parent not because the context is required for the client, but because "that is how it is related in the tables". But an API is not an ER diagram. A relationship in data is internal, while a URI should reflect how the client mentally navigates the system.
Another trap is trying to nest everything that is related to the parent, even if it is a "shared reference resource". For example, if you make tags subordinate to a task, you get a path that seems to be about tags, but is actually about "tags of one task" rather than tags as a separate area of the API. That is a different semantic, and it will easily confuse the client.
4. Task Tracker API: comments and attachments as subresources
In our project, comments and attachments are subresources of a task, and this is the canonical API model. That is, they have one "parent" — task. And this is exactly the kind of nesting that helps rather than hurts: the path is short, logical, and fully explains the meaning.
Here is what it looks like at the level of an "address map" (without tying it to Spring MVC, just as contract strings):
// Task collection and access to a task by id
String tasks = "/api/v1/tasks";
String taskById = "/api/v1/tasks/{taskId}";
// Comments: both the collection inside a task and a specific comment by id
String taskComments = "/api/v1/tasks/{taskId}/comments";
String taskCommentById = "/api/v1/tasks/{taskId}/comments/{commentId}";
// Attachments: similarly, everything lives in the task context (not the comment)
String taskAttachments = "/api/v1/tasks/{taskId}/attachments";
String taskAttachmentById = "/api/v1/tasks/{taskId}/attachments/{attachmentId}";
To see the full picture, it helps to draw it as a tree. This is not "mandatory documentation", but as a thinking tool it works great:
api/v1
└─ tasks
└─ {taskId}
├─ comments
│ └─ {commentId}
└─ attachments
└─ {attachmentId}
Notice: both comments and attachments are on the same level inside the task. This is not accidental, but a conscious defense against fragility. We do not make /tasks/{taskId}/comments/{commentId}/attachments, because such a path promises: "the attachment belongs to the comment". In our project model, the attachment belongs to the task, so we address it through the task.
And one more important detail for a beginner: the presence of {commentId} in the path does not cancel the meaning of {taskId}. Yes, the comment has its own identifier, but we care not only about the ID itself — a number or UUID — we care about the context and the boundary. A nested path is as if it tells the client: "we are working with comments inside a task; if you got the task wrong, you are in a different room altogether".
5. Useful nuances
Tags: lookup and task attribute
Tags in this project are not a separate entity with its own CRUD, but a task attribute. That is why they are deliberately made value-like: useful for filtering and display, but they do not bloat the domain into a mini-Jira with tag administration, permissions, merge rules, and other joys of adult life.
This automatically means that making /api/v1/tasks/{taskId}/tags the main path is questionable. Such an endpoint would answer the question "what tags does this task have", but that should already be visible in the task representation. The useful scenario for the client in our course is different: "give me a list of all unique tags so I can build a filter". So we get a top-level lookup resource:
// Lookup resource: a global list of tags in the system (for example, for a UI filter)
String tags = "/api/v1/tags";
If you try to nest tags under a task, the meaning becomes confusing. Compare the two lines and feel the difference in the client's head:
// Global reference data: "what tags exist at all"
String tagsLookup = "/api/v1/tags";
// Tags of a specific task: "what tags are assigned to this task"
String tagsOfOneTask = "/api/v1/tasks/{taskId}/tags";
The second line is not technically "bad". It is simply about something else, and in our API model it is not needed as a separate public path. We deliberately keep tags as a lookup and a task attribute so that we do not create entities that do not help learning the REST approach in this course right now.
Path templates in Java code
At the code level, there is no need for a new set of helper classes here. One common place is enough, where the TASKS, TASK_BY_ID, TASK_COMMENTS, TASK_ATTACHMENTS, and TAGS templates are consistently defined.
What matters is not the shape of the utility, but the discipline: subresources are built from {taskId}, while tags remains a top-level lookup. As soon as the strings start living out of sync, the API map starts drifting along with them.
6. Typical mistakes when designing nested resources
Mistake #1: nest "everything related" and end up with a URI matryoshka doll.
This is a classic: you start with /tasks/{taskId}/comments, and end up with something like /tasks/{taskId}/comments/{commentId}/attachments/{attachmentId}/versions/{versionId}. From the outside, it looks like an attempt to reflect the entire internal relationship model directly in the path. As a result, the URI becomes fragile: change one relationship — and clients have to rewrite addresses.
Mistake #2: lose the parent context for a real subresource.
Sometimes people make /comments/{commentId} simply because "a comment has a UUID, so it is possible". Technically it is possible, but from a contract perspective it is often worse: the client no longer sees which task the comment belongs to, and the API model becomes less readable. In the educational Task Tracker API, we deliberately keep comments as a supporting subresource of the task so the context is right there in the address.
Mistake #3: nest a resource under the wrong parent "because it is nearby".
Bad nesting often comes not from meaning, but from accidental proximity. For example, if an attachment belongs to a task but you somehow nest it under a comment, you have gained unnecessary depth and an incorrect boundary. This will come back later: either you will have to support two paths (chaos), or you will break the contract (even worse).
Mistake #4: make nesting not as part of the resource meaning, but as a way to "organize the code".
A URI is an external address, not a reflection of package or module structure. If you choose nesting because "it is easier to lay out controllers that way", you are confusing internal architecture with the external contract. That might work today, but when clients appear, the cost of the mistake becomes very real.
Mistake #5: allow two equal URIs for the same meaning.
For example, supporting both /tasks/{taskId}/comments/{commentId} and /comments/{commentId} as "main" paths at the same time. Clients do not understand which one is canonical, the documentation gets bloated, tests get more complicated, and you yourself start to get confused about which path to use in examples. For an educational project, canonicality matters: one scenario — one main address.
GO TO FULL VERSION