Path variable vs Query Parameters in Spring Boot
Big idea
-
Path Variable: part of the resource’s identity.
/users/{id}
→ “that specific user”. -
Query Param: filters/tweaks how you retrieve a resource (search, sort, paginate, flags).
/users?role=ADMIN&page=2&size=20
.
TL;DR table
Aspect | Path Variable (@PathVariable ) |
Query Param (@RequestParam ) |
---|---|---|
Meaning | Identifies a specific resource | Filters/options on a collection or operation |
REST URL style | /orders/{orderId} |
/orders?status=NEW&sort=date,desc |
Optionality | Usually required | Often optional (defaults allowed) |
Caching/SEO | Stable, canonical | Order/combos can vary; not canonical |
Error on missing | Typically 404 Not Found | Typically 200 OK with empty/partial results |
Length limits | Short | Can grow; for large queries use POST body |
Versioning | Avoid putting versions as query | Prefer /v1/... path segment |
Spring Boot usage
1) Path variable (resource identity)
@RestController
@RequestMapping("/api/users")
class UserController {
// GET /api/users/42
@GetMapping("/{id}")
public UserDto getById(@PathVariable long id) {
return service.findById(id); // 404 if not found
}
// Nested resource: GET /api/users/42/orders/1001
@GetMapping("/{userId}/orders/{orderId}")
public OrderDto getUserOrder(@PathVariable long userId, @PathVariable long orderId) {
return service.findOrderForUser(userId, orderId);
}
// Regex in path var (e.g., keep dots in file name)
// GET /api/files/read/report.v1.pdf
@GetMapping("/files/read/{name:.+}")
public FileDto readFile(@PathVariable String name) { ... }
}
2) Query params (filters, pagination, sorting, toggles)
@RestController
@RequestMapping("/api/users")
class UserQueryController {
// GET /api/users?role=ADMIN&page=1&size=20&active=true
@GetMapping
public Page<UserDto> search(
@RequestParam(required = false) String role,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "false") boolean active) {
return service.search(role, page, size, active);
}
// Multi-value: /api/products?tags=red&tags=summer
@GetMapping("/products")
public List<ProductDto> byTags(@RequestParam List<String> tags) {
return service.findByTags(tags);
}
// Optional + validation
@GetMapping("/filter")
public List<UserDto> filter(
@RequestParam Optional<String> city,
@RequestParam @jakarta.validation.constraints.Min(0) int minAge) {
return service.filter(city.orElse(null), minAge);
}
}
Tip: With Spring Data, you can accept
Pageable pageable
directly:GET /api/users?page=0&size=20&sort=name,asc
@GetMapping
public Page<UserDto> list(org.springframework.data.domain.Pageable pageable) {
return service.list(pageable);
}
When to use which (rules of thumb)
- Use Path Variable for:
- A single resource:
/customers/{id}
-
Hierarchies:
/shops/{shopId}/items/{itemId}
-
Versions or domains as stable segments:
/v1/...
,/india/...
- A single resource:
- Use Query Params for:
-
Search/filter:
/customers?city=Noida&minAge=25
-
Pagination/sort:
page/size/sort
-
Optional flags:
?includeInactive=true
-
Search/filter:
Pitfalls & pro tips
-
Don’t overload paths with optional pieces like
/users/ maybe-id ?
→ prefer query params for optional filters. -
Encoding:
/
in a path var must be encoded; prefer query for free-text search (q=...
). -
Large filters: if URL may exceed limits, switch to
POST /search
with a JSON body. - Consistent errors: Missing path resource → 404; filter yields none → 200 with empty list.
-
Type conversion: Spring converts to types (e.g.,
UUID
, enums) automatically: @GetMapping("/orders/{id}") public OrderDto get(@PathVariable java.util.UUID id) { ... }
-
Matrix variables exist (
@MatrixVariable
) but are rarely used on the public web; stick to query params.
Building links (server-side)
import org.springframework.web.util.UriComponentsBuilder;
// /api/users?role=ADMIN&page=1
String uri = UriComponentsBuilder.fromPath("/api/users")
.queryParam("role", "ADMIN")
.queryParam("page", 1)
.toUriString();
1) Map to different controller methods using params
Use the params
attribute on @GetMapping
/@RequestMapping
to route the same path to different methods based on query param presence or value.
@RestController
@RequestMapping("/api/reports")
class ReportController {
// GET /api/reports?type=daily
@GetMapping(params = "type=daily")
public String daily() {
return "Daily report";
}
// GET /api/reports?type=weekly
@GetMapping(params = "type=weekly")
public String weekly() {
return "Weekly report";
}
// GET /api/reports?type=monthly&format=csv
@GetMapping(params = {"type=monthly", "format=csv"})
public String monthlyCsv() {
return "Monthly CSV report";
}
// Presence-only (any value): GET /api/reports?verbose
@GetMapping(params = "verbose")
public String verboseDefault() {
return "Default report (verbose)";
}
// Explicit absence: GET /api/reports (no ?type)
@GetMapping(params = "!type")
public String defaultReport() {
return "Default report";
}
}
Notes
-
params = "q"
→ parameter must be present (any value). -
params = "!q"
→ parameter must be absent. -
params = "mode=advanced"
→ parameter must equal the given value. - Combine with arrays:
params = {"type=daily","format=json"}
. - Avoid ambiguous overlaps (e.g., a request that satisfies two methods). If needed, tighten conditions (add
!verbose
or include/require the extra param on one method).
2) Use one method and branch inside (simple switches)
Good when there are many values or you want a single entry point.
@RestController
@RequestMapping("/api/reports")
class ReportSwitchController {
// GET /api/reports?type=daily|weekly|monthly
@GetMapping
public String report(@RequestParam(required = false, defaultValue = "default") String type,
@RequestParam(required = false) String format,
@RequestParam(defaultValue = "false") boolean verbose) {
return switch (type) {
case "daily" -> verbose ? "Daily (verbose)" : "Daily";
case "weekly" -> "Weekly";
case "monthly" -> "monthly".equals(type) && "csv".equalsIgnoreCase(format) ? "Monthly CSV" : "Monthly";
default -> "Default";
};
}
}
When to pick which
-
params
on mappings: clear, declarative routing; great for a few well-defined variants. - Single method + switch: simpler when you have many values/combos or shared pre/post logic.