REST API · Lesson 33 of 95
@RequestParam
Learn @RequestParam in Spring Boot: read query parameters, set defaults, make them optional, accept lists and handle 400 errors in a shop search example.
Think about ordering food at a canteen. You say, "One masala dosa, less spicy, with extra chutney." The dish is the main request. "Less spicy" and "extra chutney" are small extra choices that change how the dish is made. Some choices you must make, and some you can skip.
A web address works like that too. After the question mark in an address like /products?category=phone, the extra choices travel as query parameters. This topic shows how @RequestParam reads them in Spring Boot.
What is @RequestParam?
A query string is the part of a URL after the ?. It is a list of name=value pairs joined with &.
text/api/products ?category=phone &page=1
Here the first pair is category=phone and the second is page=1. Each pair becomes a request parameter, and your method can read it by name. The order of the pairs does not matter.
@RequestParam gives you these settings.
required: is the parameter compulsory? The default istrue.defaultValue: the value to use when the parameter is missing or empty.name(orvalue): the parameter name, if it differs from the Java variable.
Spring converts the text to the type you write. An int, a boolean, a List<String> and an Optional<String> all work. If the conversion fails, the client gets 400 Bad Request.
Why is it used?
Some data does not belong in the path. Filters, search words, page numbers and sort order are all optional extras. A query parameter fits them well.
- Optional filters.
/productsshows everything, and/products?category=phonenarrows it down. Same endpoint, different questions. - Search. A search word like
?q=cableis one value that changes with every call. - Paging.
?page=2&size=10lets a client walk through long lists. - Defaults. You choose what happens when the client says nothing. A shop can show the first page of results, sorted by name, without the client asking for it.
- Simple testing. You can try a query parameter by typing it in the browser address bar. There is no JSON to write.
How it works
When a request arrives, Spring reads each pair from the query string and offers them to your method by name.
textGET /api/products?category=phone | v +---------------------------+ | Parse query string | | category = "phone" | +---------------------------+ | v +---------------------------+ | Bind to method parameters | | @RequestParam category | | @RequestParam maxPrice | +---------------------------+
The first parameter finds its value in the query string. The second one, maxPrice, is missing, so Spring looks at its settings next.
textIs the parameter present? | | YES NO | | v v convert Has defaultValue? to type | | | YES NO v | | method runs v v use default required? | | YES NO | | v v 400 error null
This is the whole decision. A present value is converted. A missing value falls back to defaultValue if there is one. Without a default, a required parameter gives a 400 error, and an optional one becomes null.
Real-Life Example
Think of a shopping app on your phone. You open the phone section and set "under 20,000 rupees" and "brand: any". The app sends those choices, and shows the first page of results. If you do not touch any filter, it shows all phones. The filters are optional, so the shop has defaults. But if you press "search" with an empty box, the app asks you to type a word. That is a required parameter. It cannot search for nothing.
Code Example
Let's build a product search API for TechMart. It filters by category and maximum price, pages through the results, needs a search word on one endpoint, and takes a list of ids on another.
texttechmart/ ├─ pom.xml └─ src/main/java/ └─ com/techmart/shop/ ├─ ShopApplication.java └─ ProductController.java
File: pom.xml
xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.techmart</groupId> <artifactId>shop</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
File: ShopApplication.java in package com.techmart.shop
javapackage com.techmart.shop; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class ShopApplication { public static void main(String[] args) { SpringApplication.run(ShopApplication.class, args); } }
File: ProductController.java in package com.techmart.shop
javapackage com.techmart.shop; import java.util.List; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/products") public class ProductController { record Product(int id, String name, String category, int priceInRupees) {} private final List<Product> products = List.of( new Product(1, "Nova X Phone", "phone", 18000), new Product(2, "Zen Buds", "audio", 2500), new Product(3, "Pixel Lite Phone", "phone", 26000), new Product(4, "USB-C Cable", "accessory", 400), new Product(5, "Bass Speaker", "audio", 3500)); @GetMapping public List<Product> search( @RequestParam(required = false) String category, @RequestParam(defaultValue = "100000") int maxPrice, @RequestParam(defaultValue = "0") int page) { return products.stream() .filter(p -> category == null || p.category().equals(category)) .filter(p -> p.priceInRupees() <= maxPrice) .skip(page * 2L) .limit(2) .toList(); } @GetMapping("/find") public String find(@RequestParam("q") String query) { return "Searching for " + query; } @GetMapping("/compare") public List<Product> compare(@RequestParam List<Integer> ids) { return products.stream().filter(p -> ids.contains(p.id())).toList(); } }
Start the app, then try these calls. The quotes stop the shell from treating ? and & as special.
bashmvn spring-boot:run curl "localhost:8080/api/products?category=phone" curl "localhost:8080/api/products?maxPrice=3000&page=0" curl "localhost:8080/api/products/find?q=cable" curl "localhost:8080/api/products/compare?ids=1,4"
Output: the phone filter, spaced out for reading:
json[ { "id": 1, "name": "Nova X Phone", "category": "phone", "priceInRupees": 18000 }, { "id": 3, "name": "Pixel Lite Phone", "category": "phone", "priceInRupees": 26000 } ]
Output: cheap items on the first page:
json[ { "id": 2, "name": "Zen Buds", "category": "audio", "priceInRupees": 2500 }, { "id": 4, "name": "USB-C Cable", "category": "accessory", "priceInRupees": 400 } ]
Output: search and compare. The search prints its text line, and the compare call prints the JSON below it:
textSearching for cable
json[ { "id": 1, "name": "Nova X Phone", "category": "phone", "priceInRupees": 18000 }, { "id": 4, "name": "USB-C Cable", "category": "accessory", "priceInRupees": 400 } ]
Now three calls that go wrong. A missing q, a maxPrice that is not a number, and a page beyond the end:
bashcurl -s -o /dev/null -w "%{http_code}\n" "localhost:8080/api/products/find" curl -s -o /dev/null -w "%{http_code}\n" "localhost:8080/api/products?maxPrice=abc" curl "localhost:8080/api/products?page=9"
Output:
text400 400 []
Code Explained
@RequestParam(required = false) String categoryis optional. If the client leaves it out, the value isnull, and our filter lets every product pass.defaultValue = "100000"is used whenmaxPriceis missing. It is written as text and converted toint. A parameter with a default is never treated as missing.pagedefaults to0. The code skipspage * 2items and returns two, so each page has at most two products.@RequestParam("q") String querynames the parameter. The client sendsq, and the Java variable isquery.- Without
required = falseor a default,qis required. A call without it answers 400. List<Integer> idstakes a comma-separated list.ids=1,4becomes a list of two numbers.maxPrice=abccannot become anint, so Spring answers 400.
Query Parameter Settings at a Glance
| Setting | Effect | Example |
|---|---|---|
| Nothing | Parameter required | ?q=cable needed |
required = false | Missing gives null | category above |
defaultValue | Missing gives a fallback | maxPrice above |
List<T> type | Comma list or repeated name | ids=1,4 |
Common Mistakes
- Forgetting quotes in curl. Without quotes, the shell reads
&as "run in background" and cuts your address in half. - Wrong parameter name. The client sends
querybut your annotation saysq. Spring answers 400, because the required parameter is missing. - No paging limit. A list endpoint with no size limit can return thousands of rows. Always cap the number of results.
- Using it for secrets. Query strings show up in logs and browser history. Never send passwords there.
Interview Questions
What is the difference between @RequestParam and @PathVariable?
Ans:@RequestParam reads from the query string, like ?page=2. @PathVariable reads from the path itself, like /products/7.
Is a @RequestParam required by default?
Ans:Yes. A missing required parameter gives 400 Bad Request. Set required = false or defaultValue to relax it.
How do you accept several values for one parameter?
Ans:Use a List parameter. The client sends ids=1,4 or repeats the name, as in ids=1&ids=4.
What happens if the value has the wrong type?
Ans:Spring cannot convert it and answers 400 Bad Request before your method runs.
Key Points to Remember
- A query string is the
name=valuepart after?, joined with&. @RequestParamreads those pairs into method parameters.- A parameter is required unless you set
required = falseordefaultValue. - Types convert automatically, and bad values give 400.
- Use it for filters, search words, sort order and paging.
- Quote addresses in curl when they contain
?or&.
Frequently Asked Questions
What does @RequestParam do in Spring Boot?
It reads a value from the query string, such as category=phone, and passes it to a controller method parameter, converted to the type you declare.
Can I skip @RequestParam and just name the parameter?
For simple types such as String or int, Spring still binds a parameter by name. The annotation makes the intent clear and lets you set required and defaultValue.
How do I read every parameter at once?
Declare @RequestParam Map<String, String> all. It holds every name and value from the request.
Can @RequestParam be used with POST?
Yes. It reads query parameters in any request, and it also reads the fields of a normal HTML form. For JSON bodies use @RequestBody.
Related Topics
- @PathVariable: identify one item from the address path.
- @RequestBody: read a JSON body into a Java object.
- @RequestMapping: map addresses and conditions to methods.
- HTTP Status Codes in Spring Boot: understand the 400 replies you saw here.
Practice Problems
Try each problem on your own first. Both use the same pom.xml as the TechMart example; only change the groupId and artifactId.
Easy: Library Late Fee
Riverside Library charges a late fee for every day a book is overdue. Build GET /fees that needs a days parameter and accepts an optional ratePerDay that defaults to 2 rupees. It answers with the text Late fee: 10 rupees for 5 days at the default rate.
Show answerHide answer
File: FeesApplication.java in package com.riverside.fees
javapackage com.riverside.fees; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class FeesApplication { public static void main(String[] args) { SpringApplication.run(FeesApplication.class, args); } }
File: FeeController.java in package com.riverside.fees
javapackage com.riverside.fees; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class FeeController { @GetMapping("/fees") public String fee(@RequestParam int days, @RequestParam(defaultValue = "2") int ratePerDay) { return "Late fee: " + (days * ratePerDay) + " rupees"; } }
Two calls, one with the default rate and one with a rate of 3:
bashcurl "localhost:8080/fees?days=5" curl "localhost:8080/fees?days=5&ratePerDay=3"
They print Late fee: 10 rupees and Late fee: 15 rupees. A call without days answers 400.
Medium: Dish Finder with Optional Filters
FreshBite restaurant has four dishes: Paneer Tikka (veg, 240), Chicken Curry (non-veg, 320), Veg Thali (veg, 180), Fish Fry (non-veg, 350). Build:
GET /disheswith an optionalvegfilter (truekeeps only vegetarian dishes) and asortparameter that defaults tonameand can beprice.GET /debugthat returns every query parameter it received, using aMap.
Show answerHide answer
Optional is a clean way to say "this parameter may be missing", and the Map captures whatever names the client sends.File: FreshbiteApplication.java in package com.freshbite.menu
javapackage com.freshbite.menu; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class FreshbiteApplication { public static void main(String[] args) { SpringApplication.run(FreshbiteApplication.class, args); } }
File: DishController.java in package com.freshbite.menu
javapackage com.freshbite.menu; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Optional; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class DishController { record Dish(String name, boolean veg, int priceInRupees) {} private final List<Dish> dishes = List.of( new Dish("Paneer Tikka", true, 240), new Dish("Chicken Curry", false, 320), new Dish("Veg Thali", true, 180), new Dish("Fish Fry", false, 350)); @GetMapping("/dishes") public List<Dish> dishes(@RequestParam Optional<Boolean> veg, @RequestParam(defaultValue = "name") String sort) { Comparator<Dish> order = sort.equals("price") ? Comparator.comparingInt(Dish::priceInRupees) : Comparator.comparing(Dish::name); return dishes.stream() .filter(d -> veg.isEmpty() || d.veg() == veg.get()) .sorted(order) .toList(); } @GetMapping("/debug") public Map<String, String> debug(@RequestParam Map<String, String> all) { return all; } }
Asking for vegetarian dishes sorted by price:
bashcurl "localhost:8080/dishes?veg=true&sort=price"
The reply, spaced out for reading:
json[ { "name": "Veg Thali", "veg": true, "priceInRupees": 180 }, { "name": "Paneer Tikka", "veg": true, "priceInRupees": 240 } ]
And the debug call curl "localhost:8080/debug?a=1&b=2" prints {"a":"1","b":"2"}.