Controllers and request mapping
A controller is where HTTP meets your application — the class that receives a request, calls your logic, and
returns a response. In Spring you write controllers as ordinary classes with annotations that map URLs and
HTTP methods to methods. This lesson covers the controller annotations you use constantly, building the
real ParcelController for Dakiya, verified against a running Spring Boot application.
@RestController and @RequestMapping
A REST controller is a class annotated @RestController, usually with a base path:
@RestController
@RequestMapping("/api/parcels") // base path for every method in this class
public class ParcelController {
private final ParcelRepository repository;
public ParcelController(ParcelRepository repository) { // constructor injection
this.repository = repository;
}
}
@RestController marks the class as a web controller whose method return values become the response body
(serialised to JSON), as opposed to @Controller which returns view names for server-rendered HTML — for a
REST API you want @RestController. @RequestMapping("/api/parcels") at the class level sets a base path,
so every endpoint in the class hangs off /api/parcels. The repository is injected through the constructor,
exactly as the DI lesson taught.
Mapping HTTP methods to Java methods
Each endpoint is a method annotated with the HTTP method it handles:
@GetMapping // GET /api/parcels
public List<ParcelResponse> list() { ... }
@GetMapping("/{id}") // GET /api/parcels/{id}
public ParcelResponse getOne(@PathVariable Long id) { ... }
@PostMapping // POST /api/parcels
public ResponseEntity<ParcelResponse> create(@RequestBody CreateParcelRequest req) { ... }
The mapping annotations mirror the HTTP methods: @GetMapping (read), @PostMapping (create),
@PutMapping/@PatchMapping (update), @DeleteMapping (delete). Each takes an optional path relative to
the class-level @RequestMapping, so @GetMapping("/{id}") handles GET /api/parcels/{id}. This
method-to-endpoint mapping is Spring MVC's core job: it inspects the annotations at startup, builds a table
of "this URL + method → this Java method", and routes each request accordingly. Verified against the
running app: GET /api/parcels/1 returns the parcel, POST /api/parcels creates one — each request landed
in the right method.
Getting data from the request
A controller method reads three kinds of input from the request, each with an annotation:
// path variable — part of the URL
@GetMapping("/{id}")
public ParcelResponse getOne(@PathVariable Long id) { ... } // /api/parcels/42 → id = 42
// query parameter — after the ?
@GetMapping
public List<ParcelResponse> list(@RequestParam(required = false) String city) { ... } // ?city=Pune
// request body — the JSON payload
@PostMapping
public ResponseEntity<ParcelResponse> create(@RequestBody CreateParcelRequest req) { ... }
@PathVariablebinds a{placeholder}in the path to a method parameter.GET /api/parcels/1→id = 1(verified: returned the parcel with id 1).@RequestParambinds a query-string parameter.?city=Pune→city = "Pune"(verified: the filtered list returned only Pune parcels).required = falsemakes it optional; you can also give adefaultValue.@RequestBodybinds the request's JSON body to an object — Spring deserialises the JSON into your DTO (the next lessons cover DTOs and validation).POSTwith{"recipientName":"Asha",...}→ a populatedCreateParcelRequest(verified: created a parcel from the body).
Spring picks the right binding by the annotation and converts types for you ("1" in the URL becomes a
Long id). Getting these three straight — path variable, query param, request body — is most of reading a
request.
Returning a response
The simplest thing to return is an object; Spring serialises it to JSON automatically (Jackson, from
starter-web):
@GetMapping("/{id}")
public ParcelResponse getOne(@PathVariable Long id) {
return ParcelResponse.from(...); // becomes a JSON object in the response body, 200 OK
}
Return an object → 200 with that object as JSON. Return a List → a JSON array. When you need control over
the status code or headers, wrap the body in ResponseEntity:
@PostMapping
public ResponseEntity<ParcelResponse> create(@RequestBody CreateParcelRequest req, UriComponentsBuilder uri) {
Parcel saved = repository.save(...);
var location = uri.path("/api/parcels/{id}").buildAndExpand(saved.getId()).toUri();
return ResponseEntity.created(location).body(ParcelResponse.from(saved)); // 201 + Location header
}
Verified: this returned HTTP 201 with the created parcel as JSON. ResponseEntity lets you set the
status (created = 201, ok = 200, noContent = 204) and headers (here a Location pointing at the new
resource, which REST conventions expect on a create). The api-design lesson goes deeper on which status to
return; for now, know that returning an object gives you 200, and ResponseEntity gives you full control.
The controller's real job: thin
A controller's job is HTTP plumbing — read the request, call the logic, shape the response — and not
business logic. Notice ParcelController above delegates to the repository (later, to a service) and does
no real work itself. This is the same "thin controllers" discipline the best-practices module returns to:
the controller translates between HTTP and your application, and the actual work lives in services and the
domain. A controller thick with business logic is untestable (you need HTTP to reach it) and mixes two
concerns. Keep controllers thin — map, delegate, respond — and you keep the web layer a clean boundary.
Check your work
@RestController + @RequestMapping. The class is a REST controller (method returns become the JSON
body); @RequestMapping("/api/parcels") sets a base path for all its methods.
HTTP-method mappings. @GetMapping/@PostMapping/@PutMapping/@PatchMapping/@DeleteMapping map an
HTTP method + path (relative to the class path) to a Java method. Verified routing against a running app.
Reading the request. @PathVariable (URL segment, /parcels/1), @RequestParam (query string,
?city=Pune), @RequestBody (JSON payload → DTO) — Spring binds and converts each. All verified.
Returning a response. Return an object → 200 + JSON; return List → JSON array; wrap in ResponseEntity
for explicit status/headers (verified 201 + Location on create).
Thin controllers. The controller does HTTP plumbing and delegates the work to services/domain — not business logic — keeping the web layer a clean, testable boundary.
Practice
- Build a
ParcelControllerwith@RestControllerand@RequestMapping("/api/parcels"); add a@GetMappinglist method and confirmGET /api/parcelsroutes to it. - Add
@GetMapping("/{id}")with@PathVariable Long id; request/api/parcels/1and confirm the id binds. - Add
@RequestParam(required=false) String cityto the list method; request?city=Puneand confirm it filters. - Add a
@PostMappingwith@RequestBody; POST JSON and confirm it deserialises into your object and returns 201 with aLocationheader (useResponseEntity.created(...)). - Return a plain object from a GET and confirm Spring serialises it to JSON with 200 automatically.
- Put a piece of business logic directly in a controller method, then argue where it should move and why (thin controllers).
Official documentation
- Spring — Annotated Controllers —
@RestController,@RequestMapping, method mappings. - Spring — Handler method arguments —
@PathVariable,@RequestParam,@RequestBody. - Spring — ResponseEntity — Controlling status and headers.
Next: request bodies, path variables and query parameters.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship