Spring MVC Basics: Annotations, Routing, and Request Handling
Introduction
Spring MVC is the web layer most Spring Boot REST APIs are built on. In this chapter, you will learn how requests are routed to controller methods, how common MVC annotations work, and how to receive different kinds of request data (path variables, query params, headers, body). Mastering this layer makes every later chapter (validation, exception handling, security) much easier.
Prerequisites
- Completed:
01_what_is_spring_boot.md03_create_first_spring_boot_project.md04_application_properties_and_yaml_basics.md05_profiles_for_multi_environment_config.md
- Basic Java class and annotation syntax
Where Spring MVC Fits
In a typical Boot application:
Client (Browser / Mobile / API caller)
↓ HTTP
Controller (@RestController)
↓
Service (business logic)
↓
Repository / External systemSpring MVC handles:
- URL mapping
- HTTP method dispatch (
GET,POST,PUT,DELETE) - request/response conversion (JSON, text, etc.)
@Controller vs @RestController
| Annotation | Typical use |
|---|---|
@Controller | MVC view rendering (templates like Thymeleaf/JSP) |
@RestController | REST APIs returning JSON/text directly |
@RestController is essentially:
@Controller + @ResponseBodyFor API-first backend services, @RestController is the default choice.
Mapping URLs to Methods
Spring MVC provides mapping annotations:
@RequestMapping(generic)@GetMapping@PostMapping@PutMapping@DeleteMapping
Example:
Code explanation:
- Class-level
@RequestMapping("/users")defines a common prefix - Method-level mappings define HTTP method + sub-path
@PathVariablebinds path segments like/users/100
Receive Query Parameters
Use @RequestParam for URL query parameters:
Try:
GET /search?keyword=spring&page=2&size=20Useful notes:
- Required by default
defaultValuemakes optional behavior easy- Spring converts string values to target types (
int,long, etc.)
Receive Request Headers
Use @RequestHeader:
@GetMapping("/agent")
public String userAgent(@RequestHeader(value = "User-Agent", required = false) String userAgent) {
return userAgent == null ? "unknown" : userAgent;
}Good for:
- tracing request metadata
- conditional behavior by client type
- debugging reverse proxy behavior
Receive JSON Request Body
Use @RequestBody with DTO classes:
package com.hellocode.demo.dto;
public class CreateUserRequest {
public String username;
public String email;
}Test request:
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d "{\"username\":\"alice\",\"email\":\"alice@example.com\"}"Spring Boot uses Jackson (from spring-boot-starter-web) to convert JSON <-> Java objects.
Return JSON Responses
If your method returns an object, Spring serializes it to JSON:
package com.hellocode.demo.dto;
public class UserResponse {
public Long id;
public String username;
}@GetMapping("/api/users/{id}")
public UserResponse detail(@PathVariable Long id) {
UserResponse response = new UserResponse();
response.id = id;
response.username = "demo-user";
return response;
}Result example:
{
"id": 1,
"username": "demo-user"
}Use ResponseEntity for Status and Headers
Sometimes you need full control over HTTP response:
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@PostMapping("/api/orders")
public ResponseEntity<String> createOrder() {
return ResponseEntity
.status(HttpStatus.CREATED)
.header("X-Request-Source", "spring-mvc-demo")
.body("order created");
}ResponseEntity is useful when:
- status code is not the default (
201,204, etc.) - custom headers are required
Path Matching Tips
Examples:
/users/{id}-> one path variable/teams/{teamId}/users/{userId}-> nested resources
Avoid ambiguous mappings (two handlers for same method + path), which causes startup errors.
Warning
If your app fails at startup with mapping conflicts, check duplicated controller paths first. MVC mapping collisions are one of the most common beginner errors.
Quick End-to-End Demo
You can combine all input styles in one controller:
@GetMapping("/api/demo/{id}")
public String demo(
@PathVariable Long id,
@RequestParam(defaultValue = "false") boolean verbose,
@RequestHeader(value = "X-Trace-Id", required = false) String traceId) {
return "id=" + id + ", verbose=" + verbose + ", traceId=" + traceId;
}Request example:
GET /api/demo/7?verbose=true
X-Trace-Id: abc-123Best Practices
- Use nouns in resource URLs (
/users,/orders) instead of verbs (/getUsers) - Keep controller methods thin; move business logic to service layer
- Use DTOs for request/response instead of exposing database entities directly
- Keep mapping paths explicit and consistent
- Group related endpoints with class-level
@RequestMapping
Quick Recap
In this chapter, you learned:
- Core MVC annotations and when to use them
- URL routing and HTTP method mapping
- How to read path/query/header/body data
- How to return JSON and customize responses
Next chapter will build on this by adding validation and global exception handling for production-grade APIs.
FAQ
Should I always use @RestController?
For API services, yes. For server-rendered HTML pages, use @Controller with view templates.
Can I use both @RequestMapping and @GetMapping?
Yes. A common pattern is class-level @RequestMapping("/base") plus method-level specialized mappings.
Why is my @RequestBody object null?
Usually one of these:
- missing
Content-Type: application/json - malformed JSON payload
- DTO field names do not match JSON keys
Do I need @ResponseBody with @RestController?
No. @RestController already includes it.
Where should business logic go?
Prefer service classes. Controllers should focus on HTTP concerns (input/output mapping and status handling).