What is a Controller?
In a Spring Boot web application, a controller is a Java class whose job is to receive HTTP requests (like GET /users), decide what to do with them, and send back an HTTP response.
It sits between the outside world (browser, mobile app, Postman, frontend) and your internal business logic (service layer, database, etc.).
A useful analogy: think of the controller as the receptionist of your application. It picks up the "phone call" (HTTP request), figures out which "department" (service) should handle it, and then returns the final "answer" (HTTP response) to the client.
What Do Controllers Actually Do?
Controllers in Spring Boot handle four responsibilities:
- Map URLs to Java methods
- Translate HTTP requests into method parameters
- Call the business logic
- Return HTTP responses
@Controller vs @RestController
Spring gives you two main annotations for building controllers, and choosing the right one matters.
@Controller
A traditional Spring MVC controller. It usually returns a view (e.g., an HTML page rendered with Thymeleaf).
Methods typically return a String (the view name) or a ModelAndView, and data is passed to the view via a Model.
For Example:
@Controller + method returning "user-list"
→ Spring resolves this to user-list.html and renders HTML.
So the client doesn't receive JSON or raw data; they receive a fully rendered HTML page with the user list already built into it.
@RestController
A controller built specifically for REST APIs. It returns JSON or XML, not HTML.
@RestController is essentially @Controller + @ResponseBody applied automatically to every method. That means whatever your method returns (like a User object or a List<User>) is serialized directly into the HTTP response body.
In simple terms:
- Use
@Controllerwhen building server-side rendered web pages. - Use
@RestControllerwhen building REST APIs that return data (JSON/XML) to a frontend like React or a mobile app.
Example — @Controller (returns a view):
@Controller
public class UserViewController {
@GetMapping("/users")
public String listUsers(Model model) {
model.addAttribute("users", userService.getAllUsers());
return "user-list"; // resolves to user-list.html
}
}
Example — @RestController (returns JSON):
@RestController
@RequestMapping("/api/users")
public class UserRestController {
@GetMapping
public List<User> getAllUsers() {
return userService.getAllUsers(); // serialized directly to JSON
}
}
Where Controllers Fit in MVC
Spring Boot follows the Model–View–Controller (MVC) architecture:
- Model → Represents your data and business logic (JPA entities, DTOs, repositories, services)
- View → Handles how data is presented. In classic Spring MVC, this could be Thymeleaf/HTML templates. In REST APIs, the "view" is often the JSON returned to the client.
- Controller → Handles HTTP requests and responses, connecting the Model and the View
It helps to simplify this as:
Model = Data and rules. View = What the user sees. Controller = Coordinator between the user and the system.
The Three Core Purposes of a Controller
1. Routing and Handling Requests
Controllers define routes (endpoints) and attach them to Java methods using @RequestMapping on the class and method, or shortcut annotations like @GetMapping and @PostMapping.
This tells Spring: "When a request hits /api/users with method GET, call this particular Java method."
2. Interacting with the Service Layer
Controllers should stay thin. They:
- Validate and unpack the incoming request (path variables, query parameters, JSON bodies)
- Delegate to services (e.g.,
userService.createUser(user)), where the actual business logic lives - Handle high-level errors by catching exceptions and returning meaningful HTTP status codes
Controllers shouldn't contain heavy logic themselves. Instead, they call services or other components that implement the actual business rules. This separation makes your code testable, maintainable, and easier to reason about.
3. Returning Responses
Controllers craft the final HTTP response by choosing:
- The status code (
200 OK,201 Created,400 Bad Request,404 Not Found, etc.) - The body (a JSON object, a list, or nothing)
- Optional headers (like
Locationafter creating a resource)
ResponseEntity gives you fine-grained control over all of this.
Key Controller Annotations
Class-Level Annotations
-
@Controller— marks a class as a Spring MVC controller that returns views -
@RestController— marks a class as a REST controller; all methods write their return value directly to the HTTP response body
Mapping URLs: @RequestMapping
@RequestMapping can go on a class or a method.
-
On the class: sets a common base path, like
/api/users -
On methods: defines specific endpoints and/or HTTP methods, e.g.
@RequestMapping(path = "/{id}", method = RequestMethod.GET)
Most modern Spring code prefers the more specific *Mapping annotations on methods (covered next) over writing this out manually.
HTTP Method Shortcuts
These annotations are shortcuts for @RequestMapping(method = ...):
| Annotation | HTTP Method | Purpose | Idempotency |
|---|---|---|---|
@GetMapping |
GET | Read or fetch data | Yes |
@PostMapping |
POST | Create new resources | No |
@PutMapping |
PUT | Update or fully replace an existing resource | Yes |
@PatchMapping |
PATCH | Partially update a resource (e.g., only a user's email) | No |
@DeleteMapping |
DELETE | Delete a resource, usually by ID | Yes |
Idempotent means calling the operation multiple times produces the same result as calling it once.
A typical CRUD REST controller looks like this:
GET /items → list all items
GET /items/{id} → get one item
POST /items → create new item
PUT /items/{id} → update/replace item
PATCH /items/{id} → partial update
DELETE /items/{id} → delete item
Here's what that looks like in actual code:
@RestController
@RequestMapping("/items")
public class ItemController {
private final ItemService itemService;
public ItemController(ItemService itemService) {
this.itemService = itemService;
}
@GetMapping
public List<Item> getAllItems() {
return itemService.findAll();
}
@GetMapping("/{id}")
public ResponseEntity<Item> getItemById(@PathVariable Long id) {
return itemService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<Item> createItem(@RequestBody ItemCreateRequest request) {
Item created = itemService.create(request);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
@PutMapping("/{id}")
public ResponseEntity<Item> updateItem(@PathVariable Long id, @RequestBody ItemUpdateRequest request) {
Item updated = itemService.update(id, request);
return ResponseEntity.ok(updated);
}
@PatchMapping("/{id}")
public ResponseEntity<Item> partialUpdateItem(@PathVariable Long id, @RequestBody Map<String, Object> updates) {
Item patched = itemService.partialUpdate(id, updates);
return ResponseEntity.ok(patched);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteItem(@PathVariable Long id) {
itemService.delete(id);
return ResponseEntity.noContent().build();
}
}
Extracting Data: @PathVariable, @RequestParam, @RequestBody
@PathVariable
Reads data directly from the URL path.
Example:
/users/5→5is mapped to a method parameter like@PathVariable Long id. Common when working with a specific resource by ID.
@GetMapping("/users/{id}")
public User getUserById(@PathVariable Long id) {
return userService.findById(id);
}
@RequestParam
Reads query parameters from the URL.
Example:
/users?role=ADMIN&active=true→roleandactivecan be mapped with@RequestParam String role, @RequestParam boolean active. Good for optional filters, pagination, or sorting.
@GetMapping("/users")
public List<User> getUsers(
@RequestParam String role,
@RequestParam(defaultValue = "true") boolean active) {
return userService.findByRoleAndStatus(role, active);
}
@RequestBody
Reads the HTTP request body (usually JSON) and maps it to a Java object (DTO).
Typical for POST/PUT/PATCH requests, where the client sends JSON like
{ "name": "Deborah", "email": "deborah@example.com" }. Spring (via Jackson) deserializes this JSON into a Java class when you annotate a parameter with@RequestBody.
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody UserCreateRequest request) {
User newUser = userService.create(request);
return ResponseEntity.status(HttpStatus.CREATED).body(newUser);
}
ResponseEntity: Precise Control Over Responses
ResponseEntity<T> represents the entire HTTP response, giving you control over:
- Status code (200, 201, 204, 400, 404, 500, etc.)
-
Headers (like
Location, custom headers, CORS-related headers) -
Body (generic type
T— could be an object, a list, or evenVoid)
Why this matters: for success cases, you can return ResponseEntity.ok(body) or ResponseEntity.status(HttpStatus.CREATED).body(body). For errors, you can return meaningful codes, like ResponseEntity.status(HttpStatus.NOT_FOUND).build().
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
Optional<User> user = userService.findById(id);
if (user.isPresent()) {
return ResponseEntity.ok(user.get()); // 200 OK with the user in the body
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); // 404, no body
}
}
This shows that controllers aren't just about returning data — they also communicate how an operation went, using proper HTTP semantics.
Conclusion
Controllers are the entry point to your Spring Boot application. They route requests, delegate work to the service layer, and shape the final response sent back to the client. Understanding the difference between @Controller and @RestController, knowing how to extract data with @PathVariable, @RequestParam, and @RequestBody, and using ResponseEntity for precise responses will give you a solid foundation for building robust backend APIs.
Once these concepts click, you'll start seeing every Spring Boot project through the lens of: where's the controller, what does it route, and what does it hand off?
Top comments (0)