Where this fits
When a browser or another service sends an HTTP request to your Spring application, something has to catch that request, run your code, and send a reply. In Spring, the class that does the catching is called a controller. It is the front door of a web application — the layer where "a GET request just arrived for /users/42" turns into "run this Java method and return this result."
You meet controllers the moment you build anything that answers HTTP: a JSON API, a server-rendered web page, a webhook receiver. This article is about the two annotations that declare one — @Controller and @RestController — and about how Spring decides which method handles which request. That second part is called request mapping, and it is where most of the everyday work happens.
A controller is just a managed object
One idea has to be in place before the rest makes sense. Spring runs a container: an object created at startup that builds your objects, wires them together, and manages how long they live. Any object the container owns is called a bean. You almost never write new for a controller — you describe the class, and the container builds one instance and holds onto it.
To tell the container "this class is one of yours," you put an annotation on it. At startup Spring scans your packages, finds those annotations, and registers a bean for each match. @Controller is one of those annotations.
So the first thing @Controller does is ordinary: it marks the class as a bean for the container to create. But it does a second thing, and that second thing is the entire point.
What @Controller actually signals
@Controller is a stereotype — a specialized "this is a bean" marker that also tells Spring what kind of bean it is. It says: this bean handles web requests, so inspect its methods for request mappings.
Here is the smallest useful one.
@Controller
public class PageController {
@GetMapping("/hello")
public String hello() {
return "welcome";
}
}
The @GetMapping("/hello") line says this method handles GET requests for the path /hello. The method runs and returns the string "welcome".
Now the part that surprises everyone the first time. With a plain @Controller, that returned string is not the response body. It is a view name — the name of a template file (a welcome.html, say) that Spring should find, render, and send back as a full HTML page.
That default exists for a reason. @Controller was born for server-side web pages, back when a request usually meant "give me a rendered page." So its baked-in assumption is: the String you return names a page to render, not the text to send.
Telling Spring "that's the body, not a view name"
Often you do not want a rendered page. You want to send data straight back — plain text, or JSON. To flip the meaning of the return value, you add @ResponseBody.
@Controller
public class ApiController {
@GetMapping("/hello")
@ResponseBody
public String hello() {
return "welcome";
}
}
With @ResponseBody present, the return value is written directly into the HTTP response body. The client now receives the literal text welcome, with no template lookup at all.
The same rule covers objects, and this is where it earns its keep. Return a User object from a @ResponseBody method and Spring serializes it — to JSON by default — and writes that JSON to the response. This is how a Java method becomes a REST endpoint.
@GetMapping("/users/42")
@ResponseBody
public User getUser() {
return new User(42, "Ada");
}
// client receives: {"id":42,"name":"Ada"}
So the whole difference between "web page" and "API" is that one annotation. But repeating @ResponseBody on every method in an API class gets tedious fast.
@RestController: the shortcut you almost always want
@RestController is that repetition, removed.
@RestController
public class ApiController {
@GetMapping("/hello")
public String hello() {
return "welcome"; // sent as the body, not a view name
}
}
@RestController is a meta-annotation: an annotation built out of other annotations. It bundles @Controller and @ResponseBody together, and the @ResponseBody applies to every method in the class. Nothing more, nothing less.
That gives a clean rule of thumb:
-
@RestController— for APIs that return data (JSON, XML, plain text). -
@Controller— for endpoints that render server-side pages, or a class where some methods render pages and only some return data.
And it explains a classic bug. Someone builds a JSON API with @Controller, forgets @ResponseBody, and every endpoint returns a String. Spring dutifully treats each return value as a view name and tries to find a template called welcome — so instead of JSON, they get a confusing 500 error about a missing view. The fix is almost always "switch to @RestController."
Request mapping: matching a request to a method
Declaring a controller is half the job. The other half is telling Spring which requests each method should handle. That wiring is request mapping.
The general-purpose annotation is @RequestMapping, which takes the path and the HTTP method explicitly:
@RequestMapping(path = "/hello", method = RequestMethod.GET)
Because "map this path to this one HTTP verb" is so common, Spring ships a shortcut for each verb. These two lines mean exactly the same thing:
@RequestMapping(path = "/hello", method = RequestMethod.GET)
@GetMapping("/hello")
The full set of shortcuts mirrors the HTTP verbs you already know: @GetMapping, @PostMapping, @PutMapping, @PatchMapping, @DeleteMapping. Reach for these by default; fall back to the long @RequestMapping form only when you need something they do not express.
Composing paths, and reading the request
You can put @RequestMapping on the class too, to set a common prefix. Spring joins the class-level path and the method-level path together.
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.find(id);
}
@GetMapping
public List<User> list(@RequestParam(defaultValue = "0") int page) {
return userService.page(page);
}
}
The class prefix /users plus the method path /{id} gives the full mapping /users/{id}. The second method has no method-level path, so it maps to /users itself.
Two small helpers appear here, and both pull pieces of the request into method parameters:
-
@PathVariablegrabs a value out of the path. In/users/{id}, a request to/users/42bindsid = 42. The{id}in the mapping and the parameter name line up. -
@RequestParamgrabs a value from the query string. For/users?page=2, it bindspage = 2; thedefaultValuesupplies0when the parameter is absent.
That covers the normal case end to end: mark the class, map the path and verb, pull what you need out of the request.
Narrowing a mapping further
Path and verb are the usual filters, but @RequestMapping (and the shortcuts) can narrow a match on more than that. The common two:
-
consumes— only match if the request'sContent-Typefits, e.g.consumes = "application/json". -
produces— only match if the client will accept what you emit, e.g.produces = "application/json".
@PostMapping(path = "/users", consumes = "application/json")
public User create(@RequestBody User user) {
return userService.save(user);
}
Here @RequestBody is the mirror image of @ResponseBody: it takes the incoming JSON body and deserializes it into a User for you. And consumes means this method is only chosen when the caller actually sent JSON — a request with a different content type is turned away before your code runs.
When two mappings could both match
Because you can map broadly (a /{id} pattern) and narrowly (an exact /me path) in the same class, two methods can both look like valid matches for one request. Spring resolves this by specificity: a more exact mapping wins over a more general one. An exact path like /users/me beats the variable pattern /users/{id}, even though me would also satisfy {id}.
If two mappings are genuinely equally specific — a real tie Spring cannot break — it does not guess. You get an exception at request time complaining that the mapping is ambiguous. That is a signal to make one of the two more specific, not something to work around.
Putting it together
A controller is a bean that handles HTTP. @Controller marks it, and by default treats a returned String as a page to render. @ResponseBody flips that method to write its return value straight into the response, serializing objects to JSON on the way out. @RestController is simply @Controller with @ResponseBody on every method — the right default for a data API.
Request mapping is the matching layer: @RequestMapping and its per-verb shortcuts bind a path and an HTTP method to a method, class-level and method-level paths compose, and @PathVariable and @RequestParam hand you the pieces of the request you asked for. When several mappings could match, the most specific one wins. Get these two ideas — declare the controller, map the request — and the rest of Spring MVC is detail layered on top.
Top comments (0)