Home Java Spring @PathVariable vs @RequestParam: URL and Query Parameter Binding
Beginner 4 min · July 14, 2026

Spring @PathVariable vs @RequestParam: URL and Query Parameter Binding

Master Spring's @PathVariable and @RequestParam annotations.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Everything here is grounded in real deployments.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 15-20 min read
  • Basic knowledge of Spring Boot and REST APIs.
  • Familiarity with Java annotations and dependency injection.
  • Understanding of HTTP request structure (path, query parameters).
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Use @PathVariable for required, unique identifiers in the URL path (e.g., /users/{id}).
  • Use @RequestParam for optional query parameters, filters, or pagination (e.g., /users?page=1).
  • @PathVariable binds to URI template variables; @RequestParam binds to query parameters or form data.
  • Both support default values, required flags, and type conversion, but handle missing values differently.
  • Never use @PathVariable for optional parameters – it will break your API contract.
✦ Definition~90s read
What is Spring @PathVariable and @RequestParam?

@PathVariable and @RequestParam are Spring annotations that bind URL template variables and query parameters to controller method parameters, respectively.

Think of a URL like a house address.
Plain-English First

Think of a URL like a house address. @PathVariable is like the street number – it's part of the path to get there. @RequestParam is like asking 'which floor?' or 'do you have a package?' – it's extra info after a question mark. You can't have a house without a street number, but you can skip the extra questions.

Every Spring developer has written a controller that takes parameters from the URL. But I've seen more production outages from misusing @PathVariable and @RequestParam than from any other annotation. It sounds simple, but the devil's in the details.

In this article, I'll share hard-won lessons from debugging a fintech startup's API that crashed under load because someone used @PathVariable for an optional filter. You'll learn exactly when to use each annotation, how to handle edge cases, and what the official docs gloss over.

We'll cover basic binding, advanced configuration, validation, and production debugging. By the end, you'll never confuse these two again – and your APIs will be more robust for it.

Understanding @PathVariable: Binding URL Template Variables

The @PathVariable annotation binds a method parameter to a URI template variable. It's the go-to for extracting values from the URL path itself. For example, in a REST API for a payment system, you might have:

@GetMapping("/payments/{paymentId}") public Payment getPayment(@PathVariable Long paymentId) { ... }

This is straightforward, but I've seen teams misuse it for optional parameters. Remember: path variables are always required by nature – they're part of the path. If you make them optional, you're breaking REST conventions.

Spring automatically converts the string value to the target type (e.g., Long, Integer, UUID). If conversion fails, you get a TypeMismatchException. Always use wrapper types (Long, Integer) instead of primitives to allow null in case of missing values – though missing path variables are rare, they can happen with misconfigured routes.

@GetMapping("/users/{userId}/payments/{paymentId}") public Payment getPayment(@PathVariable Long userId, @PathVariable Long paymentId) { ... }

Pro tip: If you're using Spring Boot 2.2+, the annotation value is inferred from the method parameter name if compiled with -parameters. Otherwise, you must specify the name explicitly: @PathVariable("paymentId").

PaymentController.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
@RequestMapping("/api/payments")
public class PaymentController {

    @GetMapping("/{paymentId}")
    public Payment getPayment(@PathVariable Long paymentId) {
        // Always use wrapper type to avoid NPE
        return paymentService.findById(paymentId);
    }

    @GetMapping("/{paymentId}/receipt")
    public Receipt getReceipt(@PathVariable("paymentId") Long paymentId) {
        return paymentService.getReceipt(paymentId);
    }
}
⚠ Primitive Type Pitfall
📊 Production Insight
In a production incident, I once saw a path variable bound to an empty string because of a trailing slash. Use regex patterns in @RequestMapping to enforce constraints: @GetMapping("/users/{id:[0-9]+}").
🎯 Key Takeaway
Use @PathVariable for required, unique identifiers in the URL path. Always use wrapper types and explicitly name the variable if needed.

Mastering @RequestParam: Query Parameters and Form Data

The @RequestParam annotation extracts values from query parameters (after ?) or form data. It's perfect for optional filters, pagination, and sorting. For example:

@GetMapping("/payments") public List<Payment> listPayments(@RequestParam(required = false, defaultValue = "0") int page, @RequestParam(required = false, defaultValue = "10") int size) { return paymentService.findAll(page, size); }

Key differences from @PathVariable: @RequestParam can be optional, has a default value, and supports multiple values (e.g., ?status=PENDING&status=COMPLETED). Use @RequestParam List<String> status to capture repeated parameters.

One common mistake: forgetting that @RequestParam is required by default. If a client omits the parameter, Spring throws MissingServletRequestParameterException. Always set required=false and provide a sensible default unless the parameter is truly mandatory.

Another gotcha: type conversion. If you expect an enum, Spring does automatic conversion if the enum has a String constructor or @JsonCreator. But for custom types, you'll need a Converter or PropertyEditor.

For form data, @RequestParam works the same way – it binds to form fields in POST requests. But for complex objects, use @ModelAttribute instead.

PaymentSearchController.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequestMapping("/api/payments")
public class PaymentSearchController {

    @GetMapping
    public List<Payment> searchPayments(
            @RequestParam(required = false, defaultValue = "0") int page,
            @RequestParam(required = false, defaultValue = "20") int size,
            @RequestParam(required = false) String status,
            @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate fromDate) {
        return paymentService.search(page, size, status, fromDate);
    }
}
💡Always Specify Default Values
📊 Production Insight
In a high-traffic system, repeated @RequestParam annotations for the same parameter (e.g., ?id=1&id=2) are common for batch operations. Spring automatically binds them to a List if declared as List<String>.
🎯 Key Takeaway
Use @RequestParam for optional query parameters. Always set required=false and a default value when appropriate. For multiple values, use List<>.

When to Use Which: A Decision Framework

Here's the hard truth: most teams get this wrong. They use @PathVariable for everything because it looks cleaner, or they use @RequestParam for everything because it's more flexible. Both are bad.

  1. Is the parameter a unique identifier for a resource? Use @PathVariable.
  2. Example: /users/{userId} – userId uniquely identifies a user.
  3. Is the parameter optional, or does it filter/modify the response? Use @RequestParam.
  4. Example: /users?role=admin – role filters the list.
  5. Is the parameter part of the resource hierarchy? Use @PathVariable.
  6. Example: /users/{userId}/orders/{orderId} – orderId is nested under userId.
  7. Is the parameter a configuration or metadata? Use @RequestParam.
  8. Example: /users?includeAddress=true – includeAddress modifies the response shape.
  9. Is the parameter required for the request to make sense? If it's in the path, use @PathVariable. If it's in the query, use @RequestParam (required=true).

I once worked on a SaaS billing system where the team used @PathVariable for an optional 'include_discount' flag. The result? A bunch of URL patterns like /invoices/{id}/{includeDiscount} that were ugly and confusing. We refactored to use @RequestParam and the API became much cleaner.

DecisionExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Good: @PathVariable for resource identifier
@GetMapping("/users/{userId}")
public User getUser(@PathVariable Long userId) { ... }

// Good: @RequestParam for optional filter
@GetMapping("/users")
public List<User> getUsers(@RequestParam(required = false) String role) { ... }

// Bad: @PathVariable for optional flag
@GetMapping("/users/{userId}/{includeAddress}") // Don't do this!
public User getUser(@PathVariable Long userId, @PathVariable boolean includeAddress) { ... }

// Good: @RequestParam for optional flag
@GetMapping("/users/{userId}")
public User getUser(@PathVariable Long userId, @RequestParam(required = false, defaultValue = "false") boolean includeAddress) { ... }
🔥REST Best Practice
📊 Production Insight
I've seen teams use @PathVariable for versioning (e.g., /v1/users). That's fine, but if you have multiple optional version parameters, use @RequestParam. Path variables are for single, required values.
🎯 Key Takeaway
Use @PathVariable for resource identifiers and @RequestParam for filters, pagination, and optional modifiers. Don't mix them up.

Advanced Binding: Custom Converters and Validation

Spring's default type conversion works for primitives, strings, dates, and enums. But what if you have a custom type like a PaymentId value object? You need a Converter.

Create a class implementing org.springframework.core.convert.converter.Converter<String, PaymentId>:

public class PaymentIdConverter implements Converter<String, PaymentId> { @Override public PaymentId convert(String source) { return new PaymentId(source); // validate and parse } }

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new PaymentIdConverter()); } }

Now you can use @PathVariable PaymentId paymentId directly.

Validation: Use Bean Validation (JSR-380) with @Validated on the controller class. For @RequestParam, add constraints like @Min, @Max, @Pattern on the method parameter:

@GetMapping("/payments") public List<Payment> listPayments( @RequestParam @Min(0) int page, @RequestParam @Max(100) @Min(1) int size) { ... }

This will throw ConstraintViolationException if validation fails. Handle it globally with @ExceptionHandler.

One gotcha: Spring's default error messages are ugly. Customize them with message attributes or a global exception handler that returns a consistent error JSON.

CustomConverterExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class PaymentId {
    private final String value;
    public PaymentId(String value) {
        // Validate format, e.g., "PAY-12345"
        if (!value.matches("PAY-\\d+")) {
            throw new IllegalArgumentException("Invalid payment ID format");
        }
        this.value = value;
    }
    // getter, equals, hashCode
}

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new Converter<String, PaymentId>() {
            @Override
            public PaymentId convert(String source) {
                return new PaymentId(source);
            }
        });
    }
}

@RestController
public class PaymentController {
    @GetMapping("/payments/{paymentId}")
    public Payment getPayment(@PathVariable PaymentId paymentId) {
        return paymentService.findById(paymentId);
    }
}
💡Validation on @RequestParam
📊 Production Insight
In a production system, avoid throwing exceptions in converters – they'll result in 500 errors. Instead, return null and let Spring handle it as a 400 Bad Request. Or better, throw a custom exception that maps to a proper HTTP status.
🎯 Key Takeaway
Use custom Converters for complex types and Bean Validation for input validation. Always register converters globally in a WebMvcConfigurer.

What the Official Docs Won't Tell You

The Spring Framework reference documentation is excellent, but it glosses over several real-world gotchas that I've encountered in production.

  1. Trailing Slash Ambiguity: A URL pattern like /users/{id}/ will match /users/123/ but also /users/ with an empty string for id. This can lead to subtle bugs. Always use regex patterns in @RequestMapping to enforce constraints: @GetMapping("/users/{id:[0-9]+}").
  2. Encoding Issues: If your path variable contains a slash (e.g., a date like 2023/12/01), Spring will reject it because the slash is a path separator. Use @RequestParam for such values, or encode the slash as %2F and configure Tomcat to allow encoded slashes: server.tomcat.internal-proxies=.* and set relaxedQueryChars.
  3. Optional @PathVariable with Map: You can use @PathVariable Map<String, String> to capture all path variables, but it's rarely needed. However, if you have optional path variables, this is the only way to handle them – and the docs don't emphasize that path variables are always required in the pattern.
  4. @RequestParam with Default Values and Type Conversion: If you set defaultValue="0" for an int parameter, but the client sends an empty string (?page=), Spring will use the default value. However, if you set defaultValue="" for a String, it becomes empty string, not null. This matters for validation.
  5. Performance Impact: In high-throughput systems, binding many @RequestParam parameters (e.g., 20+ filters) can be slow because Spring uses reflection to set each parameter. Consider using a single @ModelAttribute or a custom argument resolver for complex filters.

I learned these the hard way while debugging a payment gateway that failed under load because of path variable encoding issues. The client was sending dates with slashes, and we had to scramble to fix it.

TrailingSlashFix.javaJAVA
1
2
3
4
5
6
7
8
9
// Prevent empty path variable from trailing slash
@GetMapping("/users/{id:[0-9]+}")
public User getUser(@PathVariable Long id) {
    return userService.findById(id);
}

// Or use a regex that rejects empty
@GetMapping("/users/{id:\\d+}")
public User getUser(@PathVariable Long id) { ... }
⚠ Trailing Slash Bug
📊 Production Insight
In a high-traffic system, avoid using @RequestParam for more than 10 parameters. Use @ModelAttribute to group them into a POJO for better performance and readability.
🎯 Key Takeaway
Be aware of trailing slashes, encoding, and performance. Use regex patterns to enforce constraints and avoid ambiguous mappings.

Testing Parameter Binding: Integration Tests That Catch Issues

You should never trust that your parameter binding works without tests. Integration tests using MockMvc or WebTestClient are essential. Here's how to test both annotations:

mockMvc.perform(get("/api/users/123")) .andExpect(status().isOk());

mockMvc.perform(get("/api/users/abc")) // invalid type .andExpect(status().isBadRequest());

For @RequestParam, test with missing, empty, and multiple values:

mockMvc.perform(get("/api/payments")) .andExpect(status().isOk()); // should work with defaults

mockMvc.perform(get("/api/payments?page=1&size=abc")) .andExpect(status().isBadRequest()); // invalid size

Also test optional parameters: ensure that omitting them uses the default value.

One thing the docs don't stress: test your custom converters. If you have a PaymentId converter, test that it correctly parses valid strings and throws appropriate exceptions for invalid ones.

Use @WebMvcTest to load only the web layer, and mock the service layer. This keeps tests fast and focused.

ParameterBindingTest.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void testGetUserWithValidId() throws Exception {
        mockMvc.perform(get("/api/users/123"))
               .andExpect(status().isOk());
    }

    @Test
    void testGetUserWithInvalidId() throws Exception {
        mockMvc.perform(get("/api/users/abc"))
               .andExpect(status().isBadRequest());
    }

    @Test
    void testListUsersWithDefaults() throws Exception {
        mockMvc.perform(get("/api/users"))
               .andExpect(status().isOk())
               .andExpect(jsonPath("$.page").value(0));
    }

    @Test
    void testListUsersWithCustomParams() throws Exception {
        mockMvc.perform(get("/api/users?page=1&size=50"))
               .andExpect(status().isOk())
               .andExpect(jsonPath("$.page").value(1));
    }
}
💡Test Both Success and Failure
📊 Production Insight
I've seen teams skip testing error cases, only to discover in production that invalid parameters cause 500 errors instead of 400. Always test the unhappy path.
🎯 Key Takeaway
Write integration tests for your controllers covering valid, invalid, missing, and default parameter scenarios. Use @WebMvcTest for focused tests.
● Production incidentPOST-MORTEMseverity: high

The Case of the Missing Query Parameter

Symptom
Users saw HTTP 500 with 'MissingServletRequestParameterException' when making GET requests to /api/payments without the ?currency=USD query parameter.
Assumption
The developer assumed @RequestParam would default to null if not provided, but forgot to set required=false.
Root cause
The controller used @RequestParam String currency (required=true by default). When the client omitted the parameter, Spring threw an exception instead of handling the missing value gracefully.
Fix
Changed @RequestParam String currency to @RequestParam(required=false, defaultValue="USD") String currency and added a validation check.
Key lesson
  • Always explicitly set required and defaultValue for @RequestParam if the parameter can be missing.
  • Use @PathVariable only for required path segments – never for optional parameters.
  • Add global exception handling for MissingServletRequestParameterException to return a user-friendly error.
  • Document your API contract clearly – clients need to know which parameters are mandatory.
  • Write integration tests that omit optional parameters to catch missing defaults.
Production debug guideSymptom to Action4 entries
Symptom · 01
HTTP 400 Bad Request with 'Missing URI template variable'
Fix
Check your @RequestMapping path pattern – ensure the {variable} name matches @PathVariable name exactly. If using Spring Boot < 2.2, you needed to specify the value explicitly.
Symptom · 02
HTTP 500 with 'MissingServletRequestParameterException'
Fix
The @RequestParam is required by default. Either add required=false and a defaultValue, or ensure the client sends the parameter.
Symptom · 03
Null pointer exception when accessing @PathVariable
Fix
You might have used @PathVariable on a primitive type (e.g., int) without a default value. Use Integer or Long wrapper types to allow null.
Symptom · 04
Wrong value bound – path variable value is empty string
Fix
Check for trailing slashes in the URL. A pattern like /users/{id}/ will match /users/123/ but also /users/ with id=''. Use regex patterns or validation to reject empty values.
★ Quick Debug Cheat SheetQuick reference for common parameter binding issues.
MissingServletRequestParameterException
Immediate action
Add required=false and defaultValue to @RequestParam
Commands
Check controller: @RequestParam(required=false, defaultValue="0") int page
Check client request URL for missing parameter
Fix now
Set required=false and provide a sensible default
HttpMessageNotReadableException for @PathVariable+
Immediate action
Check the path variable type conversion
Commands
Verify the path variable type matches the URL segment type (e.g., Long for numeric)
Check for null or empty values in the path
Fix now
Use wrapper types (Long, Integer) instead of primitives
No handler found for URL with encoded characters+
Immediate action
Check for special characters in path variables
Commands
Use @PathVariable(required=false) and decode manually if needed
Configure Spring to allow encoded slashes: server.tomcat.internal-proxies=.*
Fix now
Use @RequestParam for values that may contain slashes
Feature@PathVariable@RequestParam
Binds toURI template variableQuery parameter / form data
Required by defaultYes (part of path)Yes (but can be made optional)
Default value supportNoYes, via defaultValue
Multiple valuesNot directlyYes, via List<>
Type conversionAutomaticAutomatic + custom converters
Typical use caseResource identifierFilter, pagination, optional data
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
PaymentController.java@RestControllerUnderstanding @PathVariable
PaymentSearchController.java@RestControllerMastering @RequestParam
DecisionExample.java@GetMapping("/users/{userId}")When to Use Which
CustomConverterExample.javapublic class PaymentId {Advanced Binding
TrailingSlashFix.java@GetMapping("/users/{id:[0-9]+}")What the Official Docs Won't Tell You
ParameterBindingTest.java@WebMvcTest(UserController.class)Testing Parameter Binding

Key takeaways

1
Use @PathVariable for required, unique resource identifiers in the URL path.
2
Use @RequestParam for optional query parameters, filters, and pagination with explicit defaults.
3
Always use wrapper types with @PathVariable to avoid null pointer exceptions.
4
Register custom converters for complex types and add Bean Validation for input validation.
5
Write integration tests covering valid, invalid, missing, and default parameter scenarios.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between @PathVariable and @RequestParam in Spring...
Q02SENIOR
How would you handle optional query parameters with default values in a ...
Q03SENIOR
Explain how to create a custom type converter for a @PathVariable parame...
Q01 of 03JUNIOR

What is the difference between @PathVariable and @RequestParam in Spring MVC?

ANSWER
@PathVariable extracts values from URI template variables (e.g., /users/{id}), while @RequestParam extracts values from query parameters (e.g., /users?id=123) or form data. @PathVariable is typically used for required resource identifiers, @RequestParam for optional filters or pagination.
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
Can I use @PathVariable and @RequestParam together in the same method?
02
What happens if a @RequestParam is missing and not required?
03
How do I bind all query parameters to a Map?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Everything here is grounded in real deployments.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Spring Boot. Mark it forged?

4 min read · try the examples if you haven't

Previous
Spring @RequestBody and @ResponseBody: Request-Response Body Binding
97 / 121 · Spring Boot
Next
REST API Pagination in Spring: Pageable, PagedModel, and Custom Strategies