1. Signature as an API contract
When you first look at Spring MVC, it is very tempting to think that an endpoint contract is only the @GetMapping("/something") annotation, and that the method inside is just "ordinary Java code." But in web development, a controller method is not just a method: it is a form of public agreement between client and server. And the signature, meaning the arguments plus the return type, makes that agreement concrete.
If we simplify it into a working model, annotations tell Spring, "which request belongs here," and the signature says, "what I need to extract from the request so that this method can be called at all," and "what needs to be placed in the response after execution." So the signature is not decoration and not a "Java formality," but the second half of the public API interface, right next to the URI.
It is also important to understand one more thing: Spring prepares arguments before the method is called. That means you do not "receive HttpServletRequest and parse it by hand" like in the old legends. Spring looks at the method parameters, the annotations on them, and tries to resolve them from the request. If that is impossible, the method may not even start, and that is normal: the API boundary must be strict.
Signature: input and output
A Spring MVC controller method signature is a bit like a well-organized order at a cafe. You do not just say, "make me something tasty," you specify the details: what should be brought to you (arguments) and what you want to receive in the end (return value). If you do not specify the order, the waiter (Spring) will either bring the wrong thing or stop and say, "I did not understand what you wanted."
At our current level, it is enough to keep a simple model in mind: first, mapping selects the method, then Spring prepares the arguments, then the method code runs, then the result is turned into an HTTP response. By "turned into," we will keep things very down to earth for now: "what the method returned goes to the client." How exactly that transformation works technically is a separate topic, and today we are not diving into it.
Below is a small matching table that helps avoid confusion. This is not a full reference, because nobody would survive that, but it is enough for today:
| What we write in the method | Where Spring gets it from | Minimal example |
|---|---|---|
| a method without arguments | from nowhere — the request is self-contained | public String ping() |
| @PathVariable String taskId | from the path, from the {taskId} segment | GET /tasks/{taskId} |
| returned String | becomes the response body | "pong" |
| returned List<String> | becomes the response body (a list) | List.of("t1", "t2") |
And yes: for now we are intentionally not touching query parameters, body, headers, and other input sources. Today, the basic skill matters: seeing how a piece of the URL ends up in a method argument.
2. Collection: GET /api/v1/tasks
A resource list (collection) is the happy scenario where we can write a method without input arguments and not feel guilty about it. If a client makes a GET /api/v1/tasks request, it is not addressing a specific task, but asking for the whole "task collection." At this level, we do not need taskId, because the client did not send one.
It is important to feel the difference: the absence of arguments is not "I forgot the parameters," but a reflection of the endpoint meaning. A task list has the contract: "give me the list." And since the request does not contain an identifier for a specific resource, the signature does not require one either. What to return is already our responsibility as an API.
A minimal example of a list endpoint (in educational form, still without services and real models):
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController // Tell Spring this is a REST controller (the response goes to the HTTP response body)
class DemoController {
@GetMapping("/api/v1/tasks") // Mapping for the collection request
public List<String> listTaskIds() {
// For the example, return "toy" task ids
return List.of("t1", "t2");
}
}
What matters here is not the contents of the list (it is still toy data), but the fact itself: the method accepts nothing because the request carries no input data in the path. The client asked for a collection, and at this level we answer with a collection. Yes, later we will have proper structures, but the principle will not change.
3. @PathVariable: taskId from the URL
When we move from a collection to a specific resource, the key REST idea appears: addressing. A specific task lives at /api/v1/tasks/{taskId}, and the client is literally saying: "give me this thing at this address." It is like an apartment number: building, floor, apartment are not filters and not wishes, but an exact address.
In Spring MVC, the "apartment number" is placed into a method argument through @PathVariable. In the URL, you write a placeholder in curly braces, and in the method, you write a parameter with an annotation. And Spring does magic... or, more precisely, does very concrete work: it takes the value from the URL and puts it into the argument.
A mental picture could look like this:
flowchart LR
A["GET /api/v1/tasks/abc-123"] --> B["mapping: /api/v1/tasks/{taskId}"]
B --> C["@PathVariable taskId = 'abc-123'"]
C --> D["controller method call"]
A minimal detail endpoint example:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@GetMapping("/api/v1/tasks/{taskId}") // The URL template contains the {taskId} placeholder
public String getTask(@PathVariable String taskId) { // Spring will take the value from the URL and put it here
// For clarity, return what came in through the path variable
return "taskId=" + taskId;
}
Here it is very useful to stop and check yourself: taskId is not "some variable in the code," but part of the URL. The client sent it in the path, Spring extracted it from the path, and now you get the ready value in the method. You do not parse the string by hand, you do not use substring, you do not write split("/"), and the world becomes a little kinder.
Names: when you need @PathVariable("taskId")
The most common small but annoying problem with @PathVariable is a name mismatch. In the URL template you wrote {taskId}, but the method parameter is String id. For a person, this is "what difference does it make, I understand it," but for Spring it sometimes looks like, "I have a variable taskId, and you are asking for id — who exactly do you mean?"
A rule that helps you stay calm: if the method parameter name matches the name in curly braces, you can write it briefly. If it does not match, it is better to specify the name explicitly in @PathVariable("..."). This is not "bureaucracy," just contract clarity. It is especially useful when you renamed a parameter in the IDE and forgot that the URL stayed the same.
Example: names match, everything is simple:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@GetMapping("/api/v1/tasks/{taskId}") // The {taskId} in the template matches the parameter name
public String getTask(@PathVariable String taskId) { // So the name does not need to be stated explicitly
// Return a string so that we can see the value in the response
return "taskId=" + taskId;
}
Example: names are different, so we specify it explicitly:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@GetMapping("/api/v1/tasks/{taskId}") // In the external contract (URL), it is still taskId
public String getTask(@PathVariable("taskId") String id) { // Explicitly bind {taskId} to the id parameter
// Inside the method, use whatever variable name is convenient for us
return "taskId=" + id;
}
Notice the subtle benefit: now the method can be read like a sentence. "We have the endpoint /tasks/{taskId}; extract taskId and put it into the variable id." That means the external contract, the word taskId, stays stable, while the internal parameter name can be anything you like: id, taskIdentifier, or pleaseDontNameMeX (but not the last one, please do not be a villain).
4. Return value and response
The return type of a controller method is also part of the contract. Not in the sense that the client sees the Java type, no, but in the sense of what shape of data you send outward. In @RestController, the return value becomes the HTTP response body. That means a simple thing: by choosing String, or List<...>, or some object, you are effectively choosing what the response will look like.
At today's level, it is enough to understand two points. First, if the method completes successfully and returns a value, the client will usually get 200 OK and some body. Second, the shape of the response body is directly tied to what you returned from the method. So the return type should be treated not as "well, let it be Object," but as a public decision.
A couple of mini examples will help you feel the difference.
If you return String, you send plain text:
import org.springframework.web.bind.annotation.GetMapping;
@GetMapping("/api/v1/ping") // Simple health-check endpoint
public String ping() {
// Return ordinary text (later it becomes the HTTP response body)
return "pong";
}
If you return Map, you send structured data (usually the client will see a JSON object):
import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;
@GetMapping("/api/v1/demo") // Endpoint that returns an "object"
public Map<String, Object> demo() {
// Map is convenient as a quick educational "DTO"
return Map.of(
"status", "OK",
"count", 2
);
}
If you return List<String>, you send a collection (usually this will be an array of values):
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
@GetMapping("/api/v1/demo-list") // Endpoint that returns an "array"
public List<String> demoList() {
// A list in the response is usually serialized as a JSON array
return List.of("a", "b", "c");
}
Notice that we are intentionally not discussing "how exactly it turns into JSON" and what settings are involved. What matters now is something else: the return type is not an implementation detail, but a decision about the response shape. And even if you are returning simple strings "for warm-up" right now, the habit of thinking about response shape will help you at every next step.
5. Mini build: tasks through signatures
To make the picture complete, let us assemble a small DemoTaskController. This is still an educational mini build: it exists only so that we can see the contrast between a collection endpoint and a specific resource endpoint.
Below is an example controller where the data is still "artificial," but the signatures very honestly reflect the meaning of the API. Such code is convenient to keep as an intermediate skeleton: it is readable in 15 seconds, and you immediately understand that the API has a list endpoint and a detail endpoint. And if you understand that from code, then it will also be easier for the client to understand it from the URI.
import java.util.List;
import org.springframework.web.bind.annotation.*;
@RestController // REST controller: what we return from methods goes into the HTTP response
@RequestMapping("/api/v1/tasks") // Base path for all endpoints in this controller
class DemoTaskController {
@GetMapping // GET /api/v1/tasks — collection
public List<String> getAllTasks() {
// Collection: there is no input data, so the signature has no arguments
return List.of("t1", "t2");
}
@GetMapping("/{taskId}") // GET /api/v1/tasks/{taskId} — specific resource
public String getTaskById(@PathVariable String taskId) {
// Detail: to address the resource, taskId from the URL is needed
return "taskId=" + taskId;
}
}
Notice how "talkative" the two signatures are. The first one: nothing is needed on input, because this is a collection. The second one: taskId is needed, because this is a separate resource. And this is exactly the REST thinking we established in earlier lessons: a collection and a single resource are different API points.
6. Typical mistakes when working with @PathVariable
There is almost no "complicated magic" in @PathVariable, but there are small traps that are easy to fall into, especially when you are just starting. The irony is that the mistakes look trivial (a letter does not match, a bracket is forgotten), but the result feels like "Spring is broken" and "nothing works." In reality, everything is working — the contract just became contradictory, and the framework is honestly refusing to guess your intentions.
Mistake #1: the path placeholder and the method parameter have different names, but the name is not specified explicitly.
You write a mapping "/{taskId}", but in the method the parameter is named id and is marked simply with @PathVariable. Depending on the conditions, this may lead to Spring not being able to understand which value to inject. A habit that saves you: either name the parameter the same way (taskId), or always write @PathVariable("taskId") String id.
Mistake #2: curly braces are forgotten, or the slash is accidentally "eaten."
Sometimes people write @GetMapping("/taskId") instead of @GetMapping("/{taskId}"). For a person, this is "almost the same thing," but for routing these are two different worlds. In the first case, the URL must literally be /taskId, while in the second case it can be any value. If an endpoint is not being called, the first thing to check is whether the mapping template is correct.
Mistake #3: trying to make a detail endpoint without @PathVariable, but with a hardcoded value inside.
A classic educational example: the method is called getTaskById, but inside it always returns the same task because taskId is not accepted anywhere. That breaks the meaning of the API: the client addresses a specific resource, while the server pretends not to notice the address. The correct minimalism is to accept taskId as an argument, even if for now you only print it in the response.
Mistake #4: the return type is chosen "by eye" rather than by the meaning of the response.
A beginner often thinks: "Well, I will return Object, and we will figure it out later." In @RestController, this is a bad habit: you lose contract clarity, and the method becomes unreadable. It is much more useful to choose a specific type: String for plain text, List<String> for a list, Map<String, Object> for a simple structured response. Even in educational examples, this trains response-shape discipline.
Mistake #5: the signature is overloaded "just in case."
Sometimes parameters are added to a method that are not needed right now: "what if they come in handy." In the end, the method starts to look like a beginner hiker's backpack, carrying a tent, a guitar, a pot, and an iron. At this stage, keep signatures short: for a collection, no arguments; for a single resource, one @PathVariable. This makes both the code and the contract obvious.
GO TO FULL VERSION