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

Request bodies, path variables and query parameters

The previous lesson introduced the three ways a controller reads input — path variables, query parameters, and the request body. This lesson goes deeper on each: the details that matter in real APIs, the type conversion Spring does, and how JSON becomes a Java object and back. These are verified against the running Dakiya API.

Path variables: identifying a resource

A path variable is part of the URL path, used to identify which resource you mean:

@GetMapping("/{id}")
public ParcelResponse getOne(@PathVariable Long id) { ... }        // GET /api/parcels/42

{id} in the path binds to the id parameter. Spring converts the type for you — the URL segment "42" (a string) becomes a Long (verified: /api/parcels/1 bound id = 1 and returned that parcel). If the conversion fails (/api/parcels/abc), Spring returns a 400 automatically. You can have several: @GetMapping("/{hubId}/parcels/{parcelId}") binds both. If the variable name differs from the parameter name, be explicit: @PathVariable("id") Long parcelId. Use path variables for the identity of a resource — the thing that says which one — not for filters or options.

Query parameters: filtering, sorting, options

A query parameter comes after the ? and is for modifying a request — filtering, sorting, paging, optional flags:

@GetMapping
public List<ParcelResponse> list(
        @RequestParam(required = false) String city,           // ?city=Pune
        @RequestParam(defaultValue = "0") int page) {          // ?page=2, defaults to 0
    ...
}

@RequestParam binds a query-string value. Three options you use constantly:

  • required = false — the parameter is optional; absent means null (verified: omitting ?city returned all parcels, ?city=Pune filtered to Pune).
  • defaultValue = "0" — a value used when absent (and it implies optional).
  • Type conversion again: ?page=2 binds to an int automatically.

The distinction from path variables matters for clean API design: path = which resource, query = how/which subset. /api/parcels/42 is the parcel 42; /api/parcels?city=Pune&status=BOOKED is a filtered collection. Putting a filter in the path, or an identity in the query, reads wrong and confuses clients.

The request body: JSON in

For POST and PUT, the data comes in the request body as JSON, bound with @RequestBody:

@PostMapping
public ResponseEntity<ParcelResponse> create(@RequestBody CreateParcelRequest req) {
    // req.recipientName(), req.destinationCity() — populated from the JSON
}

Spring deserialises the JSON body into your object using Jackson (from starter-web). Posting {"recipientName":"Asha","destinationCity":"Pune"} populates a CreateParcelRequest with those fields (verified: created a parcel from exactly that body). The binding is by field/record-component name — the JSON key recipientName maps to the recipientName component. Jackson handles the types (numbers, booleans, nested objects, lists). You almost never parse JSON by hand in Spring; you declare the shape you expect (a DTO — next lesson) and @RequestBody fills it.

The response body: JSON out

Returning is the mirror image — Jackson serialises your returned object to JSON:

@GetMapping("/{id}")
public ParcelResponse getOne(@PathVariable Long id) {
    return ParcelResponse.from(parcel);       // → {"id":1,"trackingCode":"PKG-...","status":"BOOKED",...}
}

Verified: returning a ParcelResponse produced the JSON object shown, and returning a List<ParcelResponse> produced a JSON array. The mapping is again by property name (a record's components, or a class's getters). Jackson serialises nested objects and collections recursively, formats enums as their name (Status.BOOKED → "BOOKED"), and handles dates per your configuration. So the round trip is symmetric: @RequestBody turns incoming JSON into your object; returning an object turns it back into JSON — and you work in Java types throughout, never touching the raw JSON.

Headers, and content types

Occasionally you need a request header or must set a response header:

@GetMapping("/{id}")
public ParcelResponse getOne(@PathVariable Long id,
                             @RequestHeader(value = "X-Request-Id", required = false) String requestId) {
    ...
}

@RequestHeader binds a header value. For response headers (and status), use ResponseEntity (previous lesson) — for example the Location header on a create. One content-type note: Spring reads and writes JSON by default for @RestController, driven by the Content-Type (incoming) and Accept (outgoing) headers; a client sending JSON should set Content-Type: application/json, which is why the verified curl calls included that header. You rarely configure this — JSON is the default for REST APIs and Spring handles the negotiation.

Check your work

Path variables. @PathVariable binds a {placeholder} in the URL, type-converted ("1" → Long); for the identity of a resource. Verified: /api/parcels/1 → id = 1.

Query parameters. @RequestParam binds ?key=value, with required=false/defaultValue, type- converted; for filtering/sorting/paging/options. Verified: ?city=Pune filtered, omitting it returned all.

Path versus query. Path = which resource (/parcels/42); query = how/which subset (/parcels?city=Pune) — do not swap them.

Request body. @RequestBody deserialises the JSON payload into your object (Jackson), by field name; for POST/PUT. Verified: a JSON body populated the DTO.

Response body. Returning an object serialises it to JSON (Jackson), by property name, recursively, enums as their name. Verified round trip; you work in Java types, never raw JSON.

Headers. @RequestHeader reads a header; ResponseEntity sets response headers/status; JSON is the default content type for REST controllers.

Practice

  1. Add @PathVariable Long id; request /api/parcels/abc (a non-number) and confirm Spring returns 400 on the failed conversion.
  2. Add @RequestParam(required=false) String city and @RequestParam(defaultValue="0") int page; test with and without each.
  3. Reason about (and try) a filter placed in the path vs the query — feel why /parcels?status=BOOKED reads better than /parcels/BOOKED.
  4. POST a JSON body and confirm @RequestBody populates your object; add a field to the JSON that the DTO does not have and observe it is ignored.
  5. Return a nested object (a parcel containing a list) and confirm Jackson serialises the whole structure to JSON.
  6. Read a custom request header with @RequestHeader and echo it back in the response.

Official documentation

Next: DTOs, and why you should not expose entities.

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