RizTech Academy logo
RizTech Academy
Building REST APIsLesson 1 of 630 min

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) { ... }
  • @PathVariable binds a {placeholder} in the path to a method parameter. GET /api/parcels/1 → id = 1 (verified: returned the parcel with id 1).
  • @RequestParam binds a query-string parameter. ?city=Pune → city = "Pune" (verified: the filtered list returned only Pune parcels). required = false makes it optional; you can also give a defaultValue.
  • @RequestBody binds the request's JSON body to an object — Spring deserialises the JSON into your DTO (the next lessons cover DTOs and validation). POST with {"recipientName":"Asha",...} → a populated CreateParcelRequest (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

  1. Build a ParcelController with @RestController and @RequestMapping("/api/parcels"); add a @GetMapping list method and confirm GET /api/parcels routes to it.
  2. Add @GetMapping("/{id}") with @PathVariable Long id; request /api/parcels/1 and confirm the id binds.
  3. Add @RequestParam(required=false) String city to the list method; request ?city=Pune and confirm it filters.
  4. Add a @PostMapping with @RequestBody; POST JSON and confirm it deserialises into your object and returns 201 with a Location header (use ResponseEntity.created(...)).
  5. Return a plain object from a GET and confirm Spring serialises it to JSON with 200 automatically.
  6. Put a piece of business logic directly in a controller method, then argue where it should move and why (thin controllers).

Official documentation

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