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.md
    • 03_create_first_spring_boot_project.md
    • 04_application_properties_and_yaml_basics.md
    • 05_profiles_for_multi_environment_config.md
  • Basic Java class and annotation syntax

Where Spring MVC Fits

In a typical Boot application:

text
Client (Browser / Mobile / API caller)
        ↓ HTTP
Controller (@RestController)

Service (business logic)

Repository / External system

Spring MVC handles:

  • URL mapping
  • HTTP method dispatch (GET, POST, PUT, DELETE)
  • request/response conversion (JSON, text, etc.)

@Controller vs @RestController

AnnotationTypical use
@ControllerMVC view rendering (templates like Thymeleaf/JSP)
@RestControllerREST APIs returning JSON/text directly

@RestController is essentially:

text
@Controller + @ResponseBody

For 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
  • @PathVariable binds path segments like /users/100

Receive Query Parameters

Use @RequestParam for URL query parameters:

Try:

text
GET /search?keyword=spring&page=2&size=20

Useful notes:

  • Required by default
  • defaultValue makes optional behavior easy
  • Spring converts string values to target types (int, long, etc.)

Receive Request Headers

Use @RequestHeader:

java
@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:

java
package com.hellocode.demo.dto;
 
public class CreateUserRequest {
    public String username;
    public String email;
}

Test request:

bash
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:

java
package com.hellocode.demo.dto;
 
public class UserResponse {
    public Long id;
    public String username;
}
java
@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:

json
{
  "id": 1,
  "username": "demo-user"
}

Use ResponseEntity for Status and Headers

Sometimes you need full control over HTTP response:

java
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:

java
@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:

text
GET /api/demo/7?verbose=true
X-Trace-Id: abc-123

Best 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:

  1. Core MVC annotations and when to use them
  2. URL routing and HTTP method mapping
  3. How to read path/query/header/body data
  4. 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).