Spring Boot
Path Variables
Path variables extract dynamic segments from a URI directly into handler method parameters. @PathVariable is the standard mechanism for capturing resource identifiers — IDs, slugs, usernames — from the URL path, with automatic type conversion, optional binding, and regex constraints.
@PathVariable Basics
@PathVariable binds a URI template variable — a segment defined with curly braces in the mapping pattern — to a method parameter. Spring automatically converts the string segment to the declared parameter type. By convention, when the parameter name matches the template variable name, you can omit the annotation's value attribute.
Java
@RestController
@RequestMapping("/users")
public class UserController {
// ── Basic binding — parameter name matches template variable ───────
@GetMapping("/{id}")
public UserResponse findById(@PathVariable Long id) {
// URI: GET /users/42
// id = 42L (Spring converts "42" → Long automatically)
return userService.findById(id);
}
// ── Explicit name — when parameter name differs from template var ──
@GetMapping("/{userId}")
public UserResponse findByUserId(
@PathVariable("userId") Long id) { // "userId" → id
return userService.findById(id);
}
// ── String path variable ───────────────────────────────────────────
@GetMapping("/by-email/{email}")
public UserResponse findByEmail(@PathVariable String email) {
return userService.findByEmail(email);
}
// ── Multiple path variables ────────────────────────────────────────
@GetMapping("/{userId}/orders/{orderId}")
public OrderResponse findOrder(
@PathVariable Long userId,
@PathVariable Long orderId) {
return orderService.findByUserAndId(userId, orderId);
}
// ── Enum path variable — Spring converts string to enum constant ───
@GetMapping("/role/{role}")
public List<UserResponse> findByRole(@PathVariable User.Role role) {
// GET /users/role/ADMIN → role = User.Role.ADMIN
return userService.findByRole(role);
}
}Type Conversion
Spring's ConversionService converts path variable strings to the declared parameter type automatically. Primitive types, wrappers, enums, UUID, and any type with a registered converter are all supported. A conversion failure results in a 400 Bad Request.
Java
@RestController
public class TypeDemoController {
// int / Integer
@GetMapping("/items/{page}")
public List<Item> getPage(@PathVariable int page) { ... }
// Long (most common for database IDs)
@GetMapping("/users/{id}")
public UserResponse findById(@PathVariable Long id) { ... }
// UUID — Spring converts the hyphenated string automatically
@GetMapping("/sessions/{sessionId}")
public Session findSession(@PathVariable UUID sessionId) {
// GET /sessions/123e4567-e89b-12d3-a456-426614174000
return sessionService.find(sessionId);
}
// LocalDate — requires DateTimeFormat annotation
@GetMapping("/reports/{date}")
public Report findReport(
@PathVariable
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
LocalDate date) {
// GET /reports/2024-03-15
return reportService.findByDate(date);
}
// Enum — string must match enum constant name (case-sensitive by default)
@GetMapping("/products/status/{status}")
public List<Product> findByStatus(@PathVariable ProductStatus status) {
// GET /products/status/ACTIVE → ProductStatus.ACTIVE
return productService.findByStatus(status);
}
// Conversion failure → 400 Bad Request:
// GET /users/abc (non-numeric, expected Long)
// Handled by @ExceptionHandler(MethodArgumentTypeMismatchException.class)
}
// ── Handle conversion errors globally ─────────────────────────────────
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<ErrorResponse> handleTypeMismatch(
MethodArgumentTypeMismatchException ex) {
String message = String.format(
"Path variable '%s' with value '%s' could not be converted to %s",
ex.getName(), ex.getValue(),
ex.getRequiredType() != null ? ex.getRequiredType().getSimpleName() : "unknown"
);
return ResponseEntity.badRequest()
.body(ErrorResponse.of(400, "Bad Request", message));
}
}Regex Constraints
URI template variables support inline regex constraints using the {variable:regex} syntax. Spring only routes a request to the handler when the path segment matches the regex — non-matching segments fall through to the next candidate handler (or produce a 404).
Java
@RestController
public class RegexController {
// ── Numeric IDs only ──────────────────────────────────────────────
@GetMapping("/users/{id:[0-9]+}")
public UserResponse findById(@PathVariable Long id) {
// GET /users/42 → matches
// GET /users/abc → does NOT match → 404 (not a 400)
return userService.findById(id);
}
// ── Lowercase slug ────────────────────────────────────────────────
@GetMapping("/products/{slug:[a-z0-9-]+}")
public ProductResponse findBySlug(@PathVariable String slug) {
// GET /products/blue-widget-pro → matches
// GET /products/Blue_Widget → does NOT match → 404
return productService.findBySlug(slug);
}
// ── Use regex + literal to disambiguate two handlers ──────────────
@GetMapping("/users/{id:[0-9]+}") // numeric → treat as database ID
public UserResponse findById(@PathVariable Long id) { ... }
@GetMapping("/users/{username:[a-z]+}") // alpha only → treat as username
public UserResponse findByUsername(@PathVariable String username) { ... }
// GET /users/42 → findById (numeric regex matches)
// GET /users/alice → findByUsername (alpha regex matches)
// GET /users/alice42 → 404 (neither regex matches)
// ── Multi-segment regex ────────────────────────────────────────────
@GetMapping("/files/{filename:.+}") // .+ matches filename with extension
public Resource getFile(@PathVariable String filename) {
// Without .+ Spring strips the extension from the variable value.
// GET /files/report.pdf → filename = "report.pdf"
return fileService.load(filename);
}
}Optional Path Variables
By default a path variable is required — its absence means the URI does not match the mapping pattern. To make a path variable optional, declare two separate mappings (the idiomatic Spring approach) or use Optional<T> as the parameter type.
Java
@RestController
@RequestMapping("/users")
public class UserController {
// ── Idiomatic approach: two explicit mappings ──────────────────────
@GetMapping({"", "/{id}"}) // matches /users and /users/42
public Object findByIdOrAll(
@PathVariable(required = false) Long id) {
if (id == null) {
return userService.findAll(); // /users
}
return userService.findById(id); // /users/42
}
// ── Optional<T> — cleaner null handling ───────────────────────────
@GetMapping({"", "/{id}"})
public Object findByIdOrAllOptional(
@PathVariable Optional<Long> id) {
return id.map(userService::findById)
.orElseGet(userService::findAll);
}
// ── Preferred: two separate, clearly named methods ─────────────────
@GetMapping
public List<UserResponse> findAll() {
return userService.findAll();
}
@GetMapping("/{id}")
public UserResponse findById(@PathVariable Long id) {
return userService.findById(id);
}
// Two explicit methods is more readable and avoids nullable parameters.
}Capturing All Path Variables into a Map
When a handler has many path variables — or when the variable names are dynamic — inject a Map<String, String> annotated with @PathVariable to receive all template variables at once.
Java
@RestController
public class MapBindingController {
// ── Capture all path variables into a Map ─────────────────────────
@GetMapping("/users/{userId}/orders/{orderId}/items/{itemId}")
public ItemResponse findItem(
@PathVariable Map<String, String> pathVariables) {
String userId = pathVariables.get("userId");
String orderId = pathVariables.get("orderId");
String itemId = pathVariables.get("itemId");
return itemService.find(
Long.parseLong(userId),
Long.parseLong(orderId),
Long.parseLong(itemId)
);
}
// ── Mix Map with individual bindings ──────────────────────────────
@GetMapping("/tenants/{tenantId}/users/{userId}/documents/{docId}")
public DocumentResponse findDocument(
@PathVariable Long tenantId, // individual binding
@PathVariable Map<String, String> allVars) { // all variables
String userId = allVars.get("userId");
String docId = allVars.get("docId");
// tenantId is already bound individually above
return documentService.find(tenantId, Long.parseLong(userId), docId);
}
}Path Variables vs Request Parameters
Path variables and request parameters both pass data from client to server, but they serve different semantic purposes. Choosing the right one makes your API URL design clearer and more RESTful.
Shell
# ── Path variables — identify a specific resource ─────────────────────
GET /users/42 # path variable: 42 identifies the resource
GET /users/42/orders/7 # two path variables: user 42, order 7
GET /products/blue-widget # path variable: slug identifies the product
# Path variables are for resource identity — the noun.
# The URI /users/42 names the resource. Without 42, it is a different resource.
# ── Request parameters — filter, sort, or configure a collection ───────
GET /users?role=ADMIN # filter by role
GET /users?page=2&size=10 # pagination
GET /users?sort=name,asc # sorting
GET /products?category=electronics&minPrice=100&maxPrice=500
# Request parameters are for search options — not resource identity.
# /users and /users?role=ADMIN refer to the same collection resource,
# just viewed through a different filter.
# ── Common mistake: using request params for identity ─────────────────
GET /users?id=42 # WRONG — id identifies a resource → use path variable
GET /orders?orderId=7 # WRONG
# ── Correct ────────────────────────────────────────────────────────────
GET /users/42 # RIGHT — path variable for identity
GET /orders/7 # RIGHT
# ── In Spring Boot ─────────────────────────────────────────────────────
# Path variable:
@GetMapping("/users/{id}")
public UserResponse findById(@PathVariable Long id) { ... }
# Request parameter:
@GetMapping("/users")
public Page<UserResponse> findAll(
@RequestParam(required = false) User.Role role,
Pageable pageable) { ... }