Path Variables
Read identifiers out of the URL path with @PathVariable, convert them to typed values and constrain them with patterns.
@GetMapping("/customers/{customerId}/orders/{orderId}")
OrderResponse get(@PathVariable long customerId, @PathVariable("orderId") long id) { ... }A path variable is a named segment of the URL template, written in braces: /api/customers/{customerId}/orders/{orderId}. @PathVariable binds the matched text to a method parameter and converts it to the parameter's type. Use path variables for the identity of a resource: which customer, which order. Use query parameters for options that filter or shape the result.
Names and types
When the parameter name matches the template name, @PathVariable long id is enough. That relies on the compiler keeping parameter names (the -parameters flag), which the Spring Boot parent POM and the Spring Boot Gradle plugin switch on for you. If you build some other way and see Name for argument of type [long] not specified, either add the flag or name it explicitly: @PathVariable("id").
Conversion covers numbers, UUID, enums, LocalDate and anything with a registered converter. When conversion fails, for example /api/orders/abc for a long, Spring throws MethodArgumentTypeMismatchException and the client gets 400. That is correct: the client sent a malformed request. A missing row is a different case and should be 404, which you decide in your own code.
Patterns
A variable can carry a regular expression after a colon. The mapping only matches when the segment fits, so /{id:\d+} and /{slug:[a-z][a-z0-9-]*} can live side by side on the same path. The patterns must not overlap: if a segment matches both, the request fails with an ambiguous handler error (a 500), not at startup. A request that matches neither gets 404 rather than 400, because no handler was found.
@GetMapping("/{id:\\d+}") // /api/products/42
@GetMapping("/{slug:[a-z][a-z0-9-]*}") // /api/products/blue-kettle
{*path} at the end of a pattern captures the rest of the path, slashes included, which suits file-like resources.
Common mistakes
- Using a primitive for an optional variable. Path variables are required by default; if two mappings share a method, one with the variable and one without, use a wrapper type and
required = false, or better, two methods. - Putting dots in variables.
/files/{name}withreport.pdfworks with the default path pattern parser, but older setups that enabled suffix matching cut the extension off. If you see that, look for legacy configuration. - Trusting the id. A path variable is user input. Check that the order belongs to the customer in the path, or a client can read someone else's order by changing a number.
Example
import java.math.BigDecimal;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
record Order(long id, long customerId, BigDecimal total) {}
("/api/customers/{customerId}/orders")
public class CustomerOrderController {
private final List<Order> orders = List.of(
new Order(10, 1, new BigDecimal("19.99")),
new Order(11, 2, new BigDecimal("5.00")));
// GET /api/customers/1/orders/10
("/{orderId}")
public Order get( long customerId, long orderId) {
return orders.stream()
// Both ids must match, so customer 2 cannot read customer 1's order.
.filter(o -> o.id() == orderId && o.customerId() == customerId)
.findFirst()
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
}
}When to use it
- Load /api/customers/7/orders/120 and confirm the order really belongs to customer 7 before returning it.
- Serve /api/products/42 and /api/products/blue-kettle from two methods without an ambiguous mapping.
- Return 400 automatically when a client sends a malformed UUID in /api/invoices/{id}.
More examples
Two mappings told apart by a pattern
Without the patterns both methods match <code>/api/products/42</code>. Spring does not catch that at startup, because the two templates are different strings; it fails on the first request with an ambiguous handler error and the client gets a 500. The patterns must also be disjoint: <code>[a-z0-9-]+</code> for the slug would match <code>42</code> as well, so the slug pattern here has to start with a letter. The <code>\d+</code> pattern also means <code>/api/products/abc</code> never reaches the numeric method, so it cannot fail conversion. Inside a Java string the backslash is doubled, which is easy to forget when copying a regex from documentation.
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
record Product(long id, String slug, String name) {}
@RestController
@RequestMapping("/api/products")
public class ProductLookupController {
// GET /api/products/42
@GetMapping("/{id:\\d+}")
public Product byId(@PathVariable long id) {
return new Product(id, "blue-kettle", "Blue kettle");
}
// GET /api/products/blue-kettle (a slug must start with a letter, so it never looks like an id)
@GetMapping("/{slug:[a-z][a-z0-9-]*}")
public Product bySlug(@PathVariable String slug) {
return new Product(42, slug, "Blue kettle");
}
}A UUID identifier
Random UUIDs make ids impossible to guess or count, so a competitor cannot learn how many invoices you issue by watching the numbers climb. Spring converts the segment with <code>UUID.fromString</code>; a malformed value never reaches your code. You still need an authorisation check, because an unguessable id is not access control.
import java.time.Instant;
import java.util.UUID;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
record Invoice(UUID id, String number, Instant issuedAt) {}
@RestController
public class InvoiceController {
// GET /api/invoices/3f1c9b8e-6c1d-4c55-9a5e-0d2f4b6a7e11
// GET /api/invoices/not-a-uuid -> 400, conversion fails before this method runs
@GetMapping("/api/invoices/{id}")
public Invoice get(@PathVariable UUID id) {
return new Invoice(id, "INV-2026-0042", Instant.parse("2026-10-01T09:00:00Z"));
}
}Capturing the rest of the path
<code>{*key}</code> is a capture-all variable supported by Spring's path pattern parser and is only allowed at the end of a pattern. The captured value starts with a slash, which is why the code strips the first character. Validate the result before using it to build a storage key or file path, or <code>../</code> sequences become a path traversal bug.
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
record Document(String bucket, String key) {}
@RestController
@RequestMapping("/api/files")
public class FileController {
// GET /api/files/reports/2026/q3/summary.pdf
// bucket = "reports", key = "/2026/q3/summary.pdf" (leading slash included)
@GetMapping("/{bucket}/{*key}")
public Document describe(@PathVariable String bucket, @PathVariable String key) {
return new Document(bucket, key.substring(1));
}
}
Discussion