Request Mapping & Parameters
Reviewed & published by Brayan K
Bind data from the request: @PathVariable for path segments, @RequestParam for query strings, @RequestBody for JSON payloads, and the right status codes for each operation.
Part of the free Java course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
Before You Start
Finish Building REST Controllers first — this lesson builds on @RestController, @GetMapping/@PostMapping and ResponseEntity.
What You'll Learn
- Binding path segments with @PathVariable
- Binding query strings with @RequestParam
- Optional params with required / defaultValue
- Deserialising JSON with @RequestBody
- Choosing correct HTTP status codes
- @ResponseStatus and @RequestHeader
The Big Idea — Filling Out a Delivery Form
💡 Analogy: A request is like a delivery form. The address line (path) names exactly which house — that's your @PathVariable. The delivery options ticked at the bottom (query string) are your @RequestParam. The parcel itself (the body) is your @RequestBody. Spring reads the form and hands each part to the right method parameter.
Status codes are the courier's receipt: 201 "delivered & created", 404 "no such address", 204 "done, nothing to hand back".
1️⃣ Path Variables with @PathVariable
A templated segment like /users/{id} is captured with @PathVariable. Use this to identify a specific resource — the path is part of its identity.
// (Conceptual — needs Spring Boot web on the classpath to run.)
package com.example.demo;
import org.springframework.web.bind.annotation.*;
record User(long id, String name) {}
@RestController
@RequestMapping("/users")
public class UserController {
// GET /users/42 -> {id} is captured by @PathVariable.
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return new User(id, "User-" + id);
}
}2️⃣ Query Parameters with @RequestParam
@RequestParam binds query-string values like ?q=ann&page=2. Add required=false or defaultValue to make a parameter optional.
package com.example.demo;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/users")
public class UserSearchController {
// GET /users/search?q=ann&page=2
// @RequestParam binds the query string; defaultValue makes 'page' optional.
@GetMapping("/search")
public List<String> search(
@RequestParam String q,
@RequestParam(defaultValue = "1") int page) {
return List.of("hit for '" + q + "' on page " + page);
}
}3️⃣ Request Bodies with @RequestBody
For POST/PUT, @RequestBody deserialises the JSON body into a record or POJO via Jackson. Pair it with a 201 Created status for new resources.
package com.example.demo;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
record CreateUser(String name, String email) {}
record User(long id, String name, String email) {}
@RestController
@RequestMapping("/users")
public class UserCreateController {
// POST /users with a JSON body -> @RequestBody deserialises it into the record.
@PostMapping
public ResponseEntity<User> create(@RequestBody CreateUser input) {
User saved = new User(101, input.name(), input.email());
// 201 Created is the conventional status for a successful POST.
return ResponseEntity.status(HttpStatus.CREATED).body(saved);
}
}package com.example.demo;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/users")
public class UserStatusController {
// @ResponseStatus fixes the success status without using ResponseEntity.
@PostMapping
@ResponseStatus(HttpStatus.CREATED) // 201 on success
public String create(@RequestBody String name) {
return "created: " + name;
}
// A handler can also bind a header value.
@GetMapping("/whoami")
public String whoami(@RequestHeader("X-User") String user) {
return "header says: " + user;
}
}4️⃣ The Binding, Made Concrete
Here is what each binding produces, simulated in plain Java so the path / query / body distinction is unmistakable.
import java.util.*;
public class Main {
// Simulate the binding Spring does: path id, query param, and a "body".
static String getUser(long id) { return "{\"id\":" + id + ",\"name\":\"User-" + id + "\"}"; }
static String search(String q, int page) { return "search q=" + q + " page=" + page; }
static String create(Map<String,String> body) { return "created " + body.get("name"); }
public static void main(String[] args) {
// GET /users/42 -> @PathVariable Long id = 42
System.out.println(getUser(42));
// GET /users/search?q=ann&page=2 -> @RequestParam q="ann", page=2
System.out.println(search("ann", 2));
// POST /users {"name":"Ada"} -> @RequestBody -> object
System.out.println(create(Map.of("name", "Ada")));
}
}// 🎯 YOUR TURN — replace each ___ using the hint beside it.
import java.util.*;
public class Main {
record Route(String method, String pattern) {}
static String describe(Route r) {
List<String> vars = new ArrayList<>();
for (String seg : r.pattern().split("/")) {
// 1) A {name} segment is a path variable; anything else is literal.
if (seg.startsWith("{") && seg.___("}")) vars.add(seg.substring(1, seg.length() - 1)); // 👉 replace ___ with endsWith
}
String base = r.method() + " " + r.pattern();
// 2) No braces anywhere means nothing is captured.
return vars.___() ? base + " (no path variables)" : base + " captures " + vars; // 👉 replace ___ with isEmpty
}
static int specificity(Route r) {
int score = 0;
for (String seg : r.pattern().split("/")) {
if (seg.isEmpty()) continue;
// 3) A literal segment is worth more than a variable one. That is
// the whole reason /books/featured wins over /books/{id}.
score += seg.startsWith("{") ? 1 : ___; // 👉 replace ___ with 2
}
return score;
}
public static void main(String[] args) {
List<Route> routes = new ArrayList<>(List.of(
new Route("GET", "/books/{id}"),
new Route("GET", "/books/featured"),
new Route("GET", "/books")
));
for (Route r : routes) System.out.println(describe(r));
// 4) Highest score first, so the most specific pattern is tried first.
routes.sort(Comparator.comparingInt(Main::specificity).___()); // 👉 replace ___ with reversed
System.out.println("Most specific first:");
for (Route r : routes) System.out.println(" " + r.pattern() + " (score " + specificity(r) + ")");
// 5) After sorting, the winner is simply the first one.
System.out.println("/books/featured resolves to: " + routes.get(___).pattern()); // 👉 replace ___ with 0
}
}🧠 Quick Recall
Predict before you peek.
Answer: @PathVariable for the path segment, @RequestParam for the query string.
Answer: @RequestBody deserialises it via a message converter (Jackson).
Common Errors
- ❌ Mismatched names. {id} but a differently named variable. Fix: match names or use @PathVariable("id").
- ❌ Missing required @RequestParam. Yields 400. Fix: set required=false or a defaultValue.
- ❌ Forgetting @RequestBody. The JSON isn't bound. Fix: annotate the body parameter.
- ❌ Returning 200 for everything. Misleads clients. Fix: use 201/204/404 appropriately.
- ❌ Using @RequestParam for the body. Wrong source. Fix: @RequestBody for JSON payloads.
Pro Tips
- 💡 Path for identity, query for modifiers — a clean, conventional split.
- 💡 Use defaultValue to avoid null checks on optional params.
- 💡 Validate request bodies with @Valid and Bean Validation annotations.
- 💡 Return Location headers with 201 so clients can fetch the new resource.
📋 Quick Reference
| Bind | Annotation | Source |
|---|---|---|
| path segment | @PathVariable | /users/{id} |
| query string | @RequestParam | ?q=ann&page=2 |
| JSON body | @RequestBody | POST/PUT payload |
| header | @RequestHeader | Named HTTP header |
| fixed status | @ResponseStatus | Constant code |
| created | 201 Created | Successful POST |
| missing | 404 Not Found | No such resource |
📚 Resources / Keep Learning
- 📘 Handler Method Arguments — Spring MVC docs
- 📗 Mapping Requests — Spring MVC docs
- 📗 Building REST services with Spring — tutorial
🎉 Lesson Complete!
You can now bind every part of a request — path segments (@PathVariable), query strings (@RequestParam), JSON bodies (@RequestBody) and headers (@RequestHeader) — and return correct HTTP status codes for each operation.
Practice quiz
What does @PathVariable bind?
- A query string value
- A segment of the URL path into a method parameter
- The request body
- An HTTP header only
Answer: A segment of the URL path into a method parameter. @PathVariable extracts a templated segment of the URL path (e.g. /users/{id}) into a method parameter.
Which annotation binds a query parameter like ?page=2?
- @PathVariable
- @RequestBody
- @RequestParam
- @ResponseBody
Answer: @RequestParam. @RequestParam binds query-string (or form) parameters such as ?page=2 to method arguments.
What does @RequestBody do?
- Reads a path segment
- Deserialises the HTTP request body (e.g. JSON) into an object
- Sets the status code
- Adds a header
Answer: Deserialises the HTTP request body (e.g. JSON) into an object. @RequestBody converts the incoming request body (typically JSON) into a Java object using a message converter.
For the route /users/{id}, how is id captured?
- @RequestParam Long id
- @RequestBody Long id
- @PathVariable Long id
- @RequestHeader Long id
Answer: @PathVariable Long id. A path template segment {id} is bound with @PathVariable Long id.
How do you make a @RequestParam optional with a default?
- @RequestParam(required=false) or defaultValue=...
- It cannot be optional
- Use @PathVariable
- Use @RequestBody
Answer: @RequestParam(required=false) or defaultValue=.... @RequestParam supports required=false and defaultValue to make a query parameter optional.
Which status code conventionally means a created resource?
- 200 OK
- 201 Created
- 404 Not Found
- 500 Internal Server Error
Answer: 201 Created. 201 Created indicates a new resource was successfully created, typically returned from a POST.
Which annotation sets a fixed status on a handler without ResponseEntity?
- @Scope
- @ResponseStatus
- @Qualifier
- @Primary
Answer: @ResponseStatus. @ResponseStatus(HttpStatus.CREATED) declares the status code a handler returns on success.
A request for a missing resource should typically return...
- 200 OK
- 201 Created
- 404 Not Found
- 301 Moved
Answer: 404 Not Found. 404 Not Found signals the requested resource does not exist.
For a JSON POST body mapped to an object, you use...
- @PathVariable
- @RequestParam
- @RequestBody
- @RequestHeader
Answer: @RequestBody. @RequestBody binds and deserialises the JSON request body into your DTO/record.
What does @RequestHeader bind?
- A URL path segment
- A named HTTP request header value
- The response body
- A cookie only
Answer: A named HTTP request header value. @RequestHeader binds the value of a named HTTP request header into a method parameter.
Continue this course
- Previous: Building REST Controllers
- Next: The Service Layer — @Service and separating business logic from controllers
- Quick reference: Java cheat sheet
Frequently asked questions
What is the difference between @PathVariable and @RequestParam?
They bind different parts of the URL. @PathVariable extracts a templated segment from the path itself: for the mapping /users/{id}, a request to /users/42 binds id = 42. @RequestParam binds values from the query string (or form data): for /users/search?q=ann&page=2 it binds q = "ann" and page = 2. Use @PathVariable to identify a specific resource (a path is part of the resource's identity), and @RequestParam for optional modifiers like filtering, sorting and pagination. @RequestParam also supports required=false and defaultValue to make a parameter optional, whereas a path variable is part of the route and generally required.
How does @RequestBody turn JSON into an object?
When a request carries a body — typically JSON on a POST or PUT — @RequestBody tells Spring to deserialise that body into the annotated method parameter using an HttpMessageConverter. With spring-boot-starter-web on the classpath, the default converter is Jackson, which maps JSON fields onto your record or POJO properties. So a POST to /users with body {"name":"Ada","email":"ada@x.com"} and a parameter @RequestBody CreateUser input gives you a fully populated CreateUser. It is the mirror image of how returned objects are serialised to JSON, so the same Jackson configuration governs both directions.
How should I choose HTTP status codes for my endpoints?
Follow REST conventions so clients can react correctly. 200 OK for a successful GET or an update that returns a body; 201 Created after successfully creating a resource with POST (often with a Location header); 204 No Content for a successful operation with no body, such as a DELETE; 400 Bad Request for invalid input; 404 Not Found when the requested resource does not exist; and 500 Internal Server Error for unexpected failures. In Spring you set these with ResponseEntity for per-response control, or with @ResponseStatus on the handler (or an exception) for a fixed success or error status. Meaningful status codes are part of a well-designed API contract.
How do I make a query parameter optional?
@RequestParam parameters are required by default, so a missing one yields a 400 Bad Request. To make one optional you have two options: set required = false, in which case the argument is null (or empty Optional) when absent; or supply a defaultValue, such as @RequestParam(defaultValue = "1") int page, which substitutes that value when the parameter is missing and is the cleanest approach for things like pagination. Using defaultValue keeps your handler logic simple because you never have to null-check the value. Path variables, by contrast, are part of the route and are effectively always required unless you define separate mappings.
When should I use @ResponseStatus versus ResponseEntity?
Both set the HTTP status, but at different levels. @ResponseStatus is a declarative annotation placed on a handler method (or on a custom exception class) that fixes the status for the successful (or error) case — for example @ResponseStatus(HttpStatus.CREATED) on a create method. It's concise when the status is always the same. ResponseEntity is programmatic: you decide the status, headers and body at runtime, which is essential when the same handler may return different statuses (200 if found, 404 if not) or needs to set headers like Location. Rule of thumb: use @ResponseStatus for a constant status, ResponseEntity when the response varies per request.