CodeGym /Courses /Spring REST & MVC /Resource map Task Tracker API

Resource map Task Tracker API

Spring REST & MVC
Level 3, Lesson 4
Available

1. Resource map before coding

When you start writing an API directly “in the controller,” your brain easily switches into the mode of: “okay, now I’ll make a completeTask() method… then uploadFileToTask()… then something else.” And before you know it, you no longer have an API but a collection of spells. A resource map exists to keep the project human-readable: it fixes the nouns and the relationships between them before we start arguing about implementation details.

By this point, we already have three anchors: task as the main domain object, collection vs single resource as the basic surface form, and the rule that verbs like complete, assign, upload should be translated into state changes or subresources. Now we can assemble the project map as a whole and check whether the model has started to drift.

A resource map is not a “full specification of all endpoints.” It is a short-level diagram of “which resources exist,” “which one is the main one,” “which ones live in the context of others,” and “which things we do not turn into separate entities.” You can think of it like a subway map: it does not tell you how many minutes it takes between stations, but it clearly shows where the transfer is and what routes actually exist.

It is important to state the boundary separately: precise rules for good URIs (plural form, stability, nesting depth, and so on) and the API version in the path are not needed here yet. First, we need a clean resource model; otherwise, a debate about route shape will start before it is even clear which entities live on the API surface at all.

To feel the difference between a resource map and a list of endpoints, here is a small illustration (for now, without methods and details):

import java.util.List;

// This is not "endpoints with all operations", but just the resource surface,
// that is, the list of URIs that exist in the domain at all.
List<String> resourceSurface = List.of(
    "/tasks",
    "/tasks/{taskId}",
    "/tasks/{taskId}/comments",
    "/tasks/{taskId}/attachments",
    "/tags"
);

This list is not a “finished API,” but rather a promise: this is what we expose outward. Next, we will refine it: which operations are appropriate, which responses we return, and what the data looks like. But the foundation is already here.

2. Central resource: tasks

In any sensible API — even an educational one — it is useful to have one “axis” around which everything revolves. Otherwise, the project turns into a set of equally important entities, each of which demands a full CRUD, filters, sorting, documentation, tests… and the student suddenly studies not REST design, but “how to get tired for no reason.”

In the Task Tracker API, that central resource is tasks. We start with the task collection (/tasks) and a single task (/tasks/{taskId}) because this is what the client usually wants: a list of tasks and the details of one task. It is important that a task is not “a table in the database” (we do not have a database yet), but a domain unit that the system user works with. Our job as backend developers is to make this resource read naturally.

A minimal internal model (still not a DTO and not the final shape, just “to see the resource with your eyes”) might look like this:

import java.util.UUID;

// Minimal Task form: no dates, descriptions, or other "heavy" fields —
// we want to capture the very fact of a central resource and its state.
public record Task(
    UUID id,                // task resource identifier
    String title,           // human-readable name
    TaskStatus status,      // current state in the workflow
    TaskPriority priority   // priority, so there is something to "hook onto" in the model
) {}

This model is intentionally short, because right now we are not interested in the full set of fields, but in the very fact that “Task exists as a separate entity, and it has an identifier and a state.” In a real project (per the requirements), the task will have more fields: description, deadline, assignee, tags, creation and update dates. But the important thing is different: all of that belongs to one central resource.

And here an important discipline of resource mapping appears: if Task is the main entity, then all additional parts of the domain either remain fields of Task, or become subresources next to the task (like comments and attachments), or turn into a small helper resource that does not bloat the model (like a tag lookup).

3. comments as a subresource of tasks

Comments are a classic example of something that is convenient to model as a subresource. Inside the application, comments can absolutely be separate objects, separate tables, or whatever else. But the external contract must answer the client’s question: “what is this thing and where does it live?” And in Task Tracker, the answer is quite straightforward: a comment belongs to a specific task and usually makes little sense without it.

That is why, in the resource map, comments live under the task: /tasks/{taskId}/comments. The client does not just “get comments in general”; the client gets comments for a task. This makes the API clearer, and it also helps avoid the temptation to invent “command-style” routes such as /addCommentToTask (which sound like service methods, not an external contract).

A simplified internal comment model might look like this:

import java.util.UUID;

// A comment exists "inside a task": even if it has its own id,
// its domain meaning is inseparable from taskId.
public record Comment(
    UUID id,            // identifier of a specific comment
    UUID taskId,        // link to the owning task (subresource context)
    String authorName,  // who left the comment (simplified)
    String text         // comment text
) {}

Notice taskId: even if a comment has its own identifier, it is bound to the task. From the contract perspective, that means one simple thing: we read a comment not as an independent planet, but as a satellite of the “Task planet.”

A question that often comes up here is: “Can we make /comments/{commentId}?” Yes. Sometimes even necessary (for example, if comments live separately, have an independent lifecycle, are searched globally, moderated, and so on). But in our project, that is unnecessary complexity. We deliberately make comments an auxiliary subresource: we only need listing and creating/deleting in the context of a task. And this simplicity is not “simplification out of laziness,” but a methodological choice so that the REST model remains compact.

4. attachments as a subresource

Attachments (attachments) very easily turn a project into an infrastructural abyss: files, MIME types, multipart, downloading, storage, limits… If you start digging into everything at once, you suddenly find yourself in a course on “cloud storage and file security,” even though we are actually teaching REST contracts and the web layer.

That is why, already at the resource map level, it is important to make a simple and sensible decision: attachments are a task subresource. The client thinks like this: “a task has attachments,” not “there is a global file storage where there happen to be files that can be associated with tasks.”

Hence the form: /tasks/{taskId}/attachments. This preserves the meaning: attachments live next to the task, and their lifecycle usually depends on it (if the task is deleted, the attachments are no longer needed either).

If you just want to see visually what an attachment is, you can think of it as a combination of “identifier + metadata.” Without upload/download implementation details (that comes later), a minimal metadata model might look like this:

import java.util.UUID;

// Attachments in the resource map are a Task subresource, so taskId is key here.
// Internal storage fields are intentionally not shown: the contract is about the domain, not infrastructure.
public record AttachmentMetadata(
    UUID id,                 // attachment identifier
    UUID taskId,             // link to the owning task
    String originalFileName, // original file name (for UI/humans)
    String contentType       // MIME type (minimal metadata set)
) {}

There is intentionally no “path on disk” or “storage key” here. Inside the system, that may exist, but we do not carry it outside at the contract level — and that is the right choice. The resource map currently captures the key idea: an attachment is tied to a task and addressed in its context.

Another bonus of this decision is that it fights RPC chaos in advance. Instead of a route like /uploadFileToTask, we say: “there are attachments, and they can be added to a task.” Even if, inside the service, it is indeed “file upload,” the contract remains resource-oriented.

5. tags as a lookup resource

Tags are the most deceptive API “bloat generator.” On paper it starts innocently: “well, tags exist, so let’s make /tags.” Then come “create tag,” “rename tag,” “delete tag,” “attach tag to task,” “detach tag,” “get tasks by tag”… and suddenly you are building a small reference-data management system, even though in Task Tracker tags are actually just convenient labels on tasks.

In our project, tags are first of all values inside Task. That is, tags belong to the task as a list of strings, and for the educational domain that is enough. However, a separate endpoint /tags is still useful — but in a very specific sense: as a lookup of unique values. It answers the client’s question: “what tags exist in the system at all?” so that, for example, a UI can show a suggestion.

Here is how you can think about the role of a tag in code at the most basic level:

import java.util.Set;

// Tags are not an "entity", but a set of values inside tasks.
// This is important so that the API does not grow into a separate CRUD for /tags.
public record TaskTags(
    Set<String> tags // unique string labels
) {}

This once again emphasizes the idea: a “tag” in our project is not a separate entity with its own lifecycle, but simply a string value that lives inside tasks. And the /tags endpoint is not “tag management,” but “get the list of existing tags.”

To reinforce the difference between “resource” and “resource role,” a small table is useful — it often saves the model from spreading out too much:

Part of the domain Role in the API Why
tasks main resource it is the core of the domain
comments task subresource a comment is usually not needed without a task
attachments task subresource an attachment makes sense in the context of a task
tags lookup resource we take values from tasks, so a separate CRUD is not needed

The point here is not that “this is correct according to the textbook.” The point is that this keeps the project compact and educational: we show important REST patterns, but do not turn the course into a survey of every possible domain variation.

6. status as part of Task

Task status is an excellent test of resource-oriented thinking. When you think in terms of commands, you want to do something like: /completeTask, /blockTask, /archiveTask. That sounds “clear” as long as there are few actions. But as soon as a new business operation appears (or simply a new status), the number of routes starts growing, and the client has to learn a “command vocabulary” that is not organized in any way.

In resource thinking, status is a part of the task state. That means it lives inside the Task representation — in the task data — and status changes are one type of resource update. On the resource map, that means a simple discipline: we have /tasks/{taskId}, and status is not “moved into a separate route,” but remains a task field.

We can show this in a very small example, without discussing methods or mechanics:

// Status is part of Task state, not a separate URI "resource".
public enum TaskStatus {
    TODO,        // the task is created, but work has not started
    IN_PROGRESS, // in progress
    BLOCKED,     // blocked by external reasons
    DONE,        // completed
    ARCHIVED     // moved to archive (roughly: inactive)
}

What matters here is not exactly how the status will change (that is a separate topic), but where it is conceptually “attached.” It belongs to the task and does not live as an independent contract object.

If you want a quick “filter” against RPC creep, ask yourself: “When the client looks at the path, do they see a task or do they see a command?” Compare:

// RPC approach: the URI looks like a method call.
String rpcLike = "/completeTask";

// Resource approach: the URI points to an entity, and the action is expressed by the HTTP method.
String resourceLike = "/tasks/{taskId}";

In the second option, the client immediately understands where they are in the domain. In the first, they have to remember that “completeTask” is actually related to a task, even though that is not obvious from the URI.

7. Final resource map

Now we can carefully assemble everything into a single picture. And it is exactly at this moment that it usually becomes clear how much the API is truly held together by the central resource. If the map looks like tasks in the center, with two subresources and one lookup around it, that is a good sign: the model is clear and not bloated.

Here is the basic surface (without API version and without precise URI rules; we will refine those later):

import java.util.List;

// Final list of resources that we "declare to the world".
// This is still not an endpoint map: there are no methods, response codes, or DTOs here.
List<String> surface = List.of(
    "/tasks",                         // collection
    "/tasks/{taskId}",                // single resource
    "/tasks/{taskId}/comments",       // subresource
    "/tasks/{taskId}/attachments",    // subresource
    "/tags"                           // lookup
);

And here is a visual diagram of the relationships (it is very useful to keep this in the project README or at least in notes). In mermaid, it can be written like this:

flowchart TD
    A["/tasks (task collection)"] --> B["/tasks/{taskId} (single task)"]
    B --> C["/tasks/{taskId}/comments (comments)"]
    B --> D["/tasks/{taskId}/attachments (attachments)"]
    A -. "unique values" .-> E["/tags (lookup)"]

    B --> F["status (task field, not a separate resource)"]

Notice: we even emphasize in the diagram that status is not “another path,” but a task field. It is a small detail, but it disciplines the team: less temptation to make the API “command-style.”

And one more important point: a resource map should be short. If you draw a map over two pages, and you have an educational Task Tracker with no users, no teams, no projects, and no workflow engine, that means you have started designing a product, not an educational API. In real life that happens, but in the context of the course it is an unnecessary risk: you will spend energy on scale rather than on the quality of the contract.

8. Common mistakes when creating a resource map

Mistake #1: lose the central resource and make “all entities equally important.”
Sometimes you want to make tasks, comments, attachments, and tags four identical large blocks, each with its own CRUD. That looks “symmetric,” but it works poorly both methodologically and architecturally: the project bloats, and the domain relationship becomes fuzzy. For our course, the central axis is tasks; the rest are auxiliary.

Mistake #2: turn a subresource into an independent universe for no reason.
Creating /comments/{commentId} and /attachments/{attachmentId} is tempting because of the desire to “do it like in big systems.” But in big systems, that is justified by the entity lifecycle and access scenarios. In our case, access to comments and attachments almost always starts with a task, so the subresource path makes the contract simpler, not more complicated.

Mistake #3: drift into RPC-like style because of “convenient names.”
Routes like /addCommentToTask, /uploadFileToTask, /completeTask look clear when there are only three of them. Then there are ten, then twenty, and the client has to learn a list of commands instead of navigating by the domain. If the first thing in the path is a verb, that is almost always a signal to stop and return to the resource model.

Mistake #4: make tags a full CRUD module “because that’s how it is done.”
Tags can easily become a reference catalog with their own rules, uniqueness, conflicts, admin interface… But in our project, tags are part of the task, and /tags is just a convenient lookup. As soon as you add “create/delete tag,” you are forced to answer dozens of new questions (what about tasks that used that tag? who is allowed to delete it? what does it mean to rename it?), and the course goes off track.

Mistake #5: mix up the “resource map” and the “endpoint map” too early.
Beginners often immediately add methods, statuses, content types, error details, and even DTO formats to the map. The result is not a resource map, but a half-spec, half-code, half-dream. It is better to proceed by layers: today we fix “what entities and relationships exist,” tomorrow we turn that into canonical URIs and a full route list, and only then do we start discussing implementation mechanics.

1
Task
Spring REST & MVC, level 3, lesson 4
Locked
Resource Surface of the Task Tracker API
Resource Surface of the Task Tracker API
1
Task
Spring REST & MVC, level 3, lesson 4
Locked
Generation of the resource-map.md file
Generation of the resource-map.md file
1
Survey/quiz
REST Contracts, level 3, lesson 4
Unavailable
REST Contracts
Resources, URIs, and methods
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION