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/...
  • Use Query Params for:
    • Search/filter: /customers?city=Noida&minAge=25
    • Pagination/sort: page/size/sort
    • Optional flags: ?includeInactive=true

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.

Back to blog

Leave a comment