Foundation · Build
Your First REST Controller
Route HTTP requests to Java methods with @RestController and the mapping annotations, then grow one endpoint into a small resource API.
Primary source: Spring MVC: Annotated Controllers
Lesson 1 of 11
1d streakThe Architectural Question
Until you have written one route and called it, every MVC concept floats free of the concrete request it will eventually handle.
What this guide proves
- @RestController routes
- @GetMapping and @PostMapping
- Resource URLs
HTTP dispatching: How Spring MVC maps URLs to Java methods
A REST controller declares the resource contract; it does not own the HTTP dispatch pipeline. Spring MVC routes requests through DispatcherServlet, which is explained step by step in the dedicated DispatcherServlet lesson. Keep this lesson focused on choosing resource names, HTTP methods, and a controller boundary.
Building an idiomatic REST controller
An idiomatic REST controller uses plural nouns for resource paths, enforces immutability via Java records, and delegates business state management to a domain service.
@RestController
@RequestMapping("/api/v1/tasks")
public class TaskController {
private final TaskService taskService;
// Single constructor: auto-wired by Spring without @Autowired
public TaskController(TaskService taskService) {
this.taskService = Objects.requireNonNull(taskService);
}
@GetMapping
public List<TaskResponse> listTasks() {
return taskService.findAll();
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public TaskResponse createTask(@RequestBody @Valid CreateTaskRequest request) {
return taskService.create(request);
}
}Resource URI Design
Break it deliberately: The missing @RestController trap
Annotating methods with @GetMapping or @PostMapping on a standard class without @RestController or @Controller causes Spring's RequestMappingHandlerMapping to completely ignore the class during startup.
// Missing @RestController or @Controller!
public class TaskController {
@GetMapping("/api/v1/tasks")
public List<String> getTasks() {
return List.of("Task 1", "Task 2");
}
}$ curl -i http://localhost:8080/api/v1/tasks
HTTP/1.1 404 Not Found
Content-Type: application/json
Transfer-Encoding: chunked
{
"timestamp": "2026-09-03T10:15:22.124Z",
"status": 404,
"error": "Not Found",
"path": "/api/v1/tasks"
}Root Cause
Check your understanding
Active Recall Checkpoint
What central Spring MVC servlet acts as the Front Controller for all incoming HTTP requests?
Hint: Think about the core servlet in spring-webmvc.
Show answer & explanation
DispatcherServlet acts as the Front Controller, dispatching requests to handler mappings, invoking handler adapters, and coordinating message converters.
Active Recall Checkpoint
What happens when a class declares @GetMapping methods but lacks @RestController or @Controller at class level?
Hint: Does RequestMappingHandlerMapping scan it?
Show answer & explanation
RequestMappingHandlerMapping ignores the class during startup, resulting in HTTP 404 Not Found for all routes declared on that class.
Working Implementation
Code Walkthrough
Expose GET /api/tasks returning an empty list, then POST /api/tasks that echoes the submitted title back with an id.
package com.example.web;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.net.URI;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@RestController
@RequestMapping("/api/v1/products")
public class FirstRestControllerLab {
public record Product(String id, String name, double price) {}
private final Map<String, Product> store = new ConcurrentHashMap<>();
@GetMapping("/{id}")
public ResponseEntity<Product> getProduct(@PathVariable String id) {
Product product = store.get(id);
if (product == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(product);
}
@PostMapping
public ResponseEntity<Product> createProduct(@RequestBody Product input) {
store.put(input.id(), input);
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}").buildAndExpand(input.id()).toUri();
return ResponseEntity.created(location).body(input);
}
}Synthesis & Production Verification
Production Acceptance Criteria
- Serve JSON from GET and accept JSON on POST
- Name resources with plural nouns and avoid verbs in paths
- Call both endpoints from curl and read the exchanged headers
Key Takeaway
A REST controller maps resource-oriented HTTP contracts to Java methods; handler mapping selects the method, while message converters own JSON input and output.
Architectural & Technical Review
Core Interview Questions
Q01What responsibilities should a first REST controller demonstrate?Show answer
It should make route selection, request-body binding, status and header semantics, and response serialization visible without mixing in persistence or business complexity.
Q02How do request mapping and message conversion differ?Show answer
Handler mapping selects a method from path, HTTP method, and mapping conditions. Message converters deserialize and serialize representations according to Content-Type, Accept, and Java types.
Q03What does a successful POST contract need beyond a JSON body?Show answer
It should communicate the outcome with 201 Created and identify the new resource with a Location header, then expose a representation whose fields are intentionally public.
Production Pitfalls
Critical Anti-Patterns
- Pitfall: Encoding actions in resource paths — Routes such as /createTask duplicate semantics already carried by POST and make a resource API harder to evolve consistently.
- Pitfall: Hand-building JSON strings — Manual JSON bypasses configured converters, escaping, content negotiation, and reusable contract types.
Primary Documentation & Specifications
Lesson Primary References
Supplementary Module Resources