Spring MVC Request Mapping: Paths, Params and Bodies

Introduction

This tutorial explains how Spring MVC maps an HTTP request to a controller method and turns the parts of the request into the method's arguments. We will start with @RequestMapping and its shortcut annotations, and the attributes that narrow a mapping down. We will then take values from the path with @PathVariable - including patterns and regular expressions - and from the query string with @RequestParam: required, optional, defaulted and multi-valued parameters. Next we will read headers and cookies, bind a group of query parameters to a record, and read a JSON body with @RequestBody. Lastly, we will send requests that do not fit any mapping, and see the 400, 404, 405 and 415 responses Spring answers with.

(This tutorial builds on Building a REST API with Spring Boot and @RestControllerClick to read tutorial: Building a REST API with Spring Boot and @RestController.)

@RequestMapping and its shortcuts

@RequestMapping is the general annotation: it maps requests by path and, optionally, by HTTP method and other conditions. Its shortcuts fix the method -
  • @GetMapping, @PostMapping, @PutMapping, @PatchMapping and @DeleteMapping.
A mapping can be narrowed further with these attributes, which all the shortcuts accept too - On the class, @RequestMapping sets a prefix and defaults that every method in the controller inherits.

The example controller

One controller demonstrates each way of receiving data. The sections below go through it endpoint by endpoint, with real requests against the running application -
MappingApp.java - one endpoint for each way of receiving request data
//MappingApp.java
package com.javabrahman.rest.mapping;

import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

record BookFilter(String author, Integer fromYear, Integer toYear) {
}

record OrderLine(String isbn, int quantity) {
}

record Order(String customer, List<OrderLine> lines) {
}

@RestController
@RequestMapping("/api")
class MappingController {

  // 1. Path variables
  @GetMapping("/books/{id}")
  String bookById(@PathVariable long id) {
    return "book by id: " + id;
  }

  @GetMapping("/books/isbn/{isbn:\\d{13}}")   // only 13 digits match
  String bookByIsbn(@PathVariable String isbn) {
    return "book by ISBN: " + isbn;
  }

  @GetMapping("/authors/{author}/books/{year}")
  String booksByAuthorAndYear(@PathVariable String author, @PathVariable int year) {
    return "books by " + author + " published in " + year;
  }

  @GetMapping("/files/{*path}")   // captures the rest of the path, slashes included
  String file(@PathVariable String path) {
    return "file path: " + path;
  }

  // 2. Query parameters
  @GetMapping("/search")
  String search(@RequestParam String q,
                @RequestParam(defaultValue = "1") int page,
                @RequestParam Optional<String> sort,
                @RequestParam(name = "tag", required = false) List<String> tags) {
    return "q=" + q + ", page=" + page + ", sort=" + sort.orElse("(none)") + ", tags=" + tags;
  }

  @GetMapping("/params")
  Map<String, String> allParams(@RequestParam Map<String, String> params) {
    return params;
  }

  // 3. Headers and cookies
  @GetMapping("/greeting")
  String greeting(@RequestHeader(name = "Accept-Language", defaultValue = "en") String language,
                  @RequestHeader("User-Agent") Optional<String> userAgent,
                  @CookieValue(name = "theme", required = false) String theme) {
    return "language=" + language + ", user agent present=" + userAgent.isPresent() + ", theme cookie=" + theme;
  }

  // 4. Several query parameters bound to one record
  @GetMapping("/books")
  BookFilter filter(@ModelAttribute BookFilter filter) {
    return filter;
  }

  // 5. A JSON request body
  @PostMapping(path = "/orders", consumes = "application/json")
  @ResponseStatus(HttpStatus.CREATED)
  String placeOrder(@RequestBody Order order) {
    int items = order.lines().stream().mapToInt(OrderLine::quantity).sum();
    return "order for " + order.customer() + ": " + order.lines().size() + " lines, " + items + " items";
  }
}

@SpringBootApplication
public class MappingApp {
  public static void main(String[] args) {
    SpringApplication.run(MappingApp.class, args);
  }
}

Path variables

A {name} segment in the mapping captures that part of the path, and @PathVariable passes it to the method parameter of the same name, converted to the parameter's type -  OUTPUT of requests with path variables
> curl -i http://localhost:8080/api/books/42
HTTP/1.1 200
Content-Length: 14
Content-Type: text/plain; charset=UTF-8

book by id: 42

> curl -i http://localhost:8080/api/books/isbn/9780134685991
HTTP/1.1 200
Content-Length: 27
Content-Type: text/plain; charset=UTF-8

book by ISBN: 9780134685991

> curl -i http://localhost:8080/api/books/isbn/12345
HTTP/1.1 404
Content-Type: application/json

{"timestamp":"2026-11-06T09:17:40.167Z","status":404,"error":"Not Found","path":"/api/books/isbn/12345"}

> curl -i http://localhost:8080/api/authors/bloch/books/2018
HTTP/1.1 200
Content-Length: 32
Content-Type: text/plain; charset=UTF-8

books by bloch published in 2018

> curl -i http://localhost:8080/api/files/docs/2026/guide.pdf
HTTP/1.1 200
Content-Length: 31
Content-Disposition: inline; filename=f.txt
Content-Type: text/plain; charset=UTF-8

file path: /docs/2026/guide.pdf

Explanation of the output

  • /api/books/42 converted 42 to a long.
  • {isbn:\d{13}} adds a regular expression: the 13-digit ISBN matched, while 12345 did not match any mapping, so Spring answered 404. Regular expressions keep different kinds of ids apart, and reject malformed ones before your code runs.
  • A path can hold several variables, each converted separately - a String author and an int year.
  • {*path} captures everything that remains, slashes included - starting with the slash after /files - which suits file paths and similar hierarchical names.
  • The variable name comes from the method parameter's name; this needs the compiler's -parameters option, which Spring Boot's Maven and Gradle plugins switch on. Otherwise, name it explicitly: @PathVariable("id") long id.

Query parameters

@RequestParam reads parameters from the query string - the part after ? - or from a submitted HTML form. By default a parameter is required; defaultValue, required = false or an Optional make it optional. A parameter repeated in the URL binds to a List, and a Map receives all parameters -  OUTPUT of requests with query parameters
> curl -i http://localhost:8080/api/search?q=java
HTTP/1.1 200
Content-Length: 38
Content-Type: text/plain; charset=UTF-8

q=java, page=1, sort=(none), tags=null

> curl -i http://localhost:8080/api/search?q=java&page=3&sort=title&tag=streams&tag=collections
HTTP/1.1 200
Content-Length: 55
Content-Type: text/plain; charset=UTF-8

q=java, page=3, sort=title, tags=[streams, collections]

> curl -i http://localhost:8080/api/search?page=2
HTTP/1.1 400
Content-Type: application/json

{"timestamp":"2026-11-06T09:17:47.555Z","status":400,"error":"Bad Request","path":"/api/search"}

> curl -i http://localhost:8080/api/params?category=java&level=beginner
HTTP/1.1 200
Content-Length: 38
Content-Type: application/json

{"category":"java","level":"beginner"}

Explanation of the output

  • With only q, the defaults applied: page 1 from defaultValue, an empty Optional for sort, and null for the tags.
  • The second request set every parameter; the two tag values became a list.
  • Without the required q, Spring answered 400 Bad Request before calling the method.
  • The Map parameter received every query parameter, which Spring then wrote back as a JSON object.

Headers and cookies

@RequestHeader and @CookieValue work the same way as @RequestParam, for request headers and cookies -  OUTPUT of requests with headers and a cookie
> curl -i http://localhost:8080/api/greeting
HTTP/1.1 200
Content-Length: 56
Content-Type: text/plain; charset=UTF-8

language=en, user agent present=false, theme cookie=null

> curl -i http://localhost:8080/api/greeting -H "Accept-Language: fr-FR" -H "Cookie: theme=dark"
HTTP/1.1 200
Content-Length: 59
Content-Type: text/plain; charset=UTF-8

language=fr-FR, user agent present=false, theme cookie=dark
Without the headers, Accept-Language fell back to its default, and the missing cookie gave null; with them, both values arrived. The User-Agent header was absent in both requests from this test client. Use headers for metadata about a request - language, tracing ids, client versions - and keep business data in the path, query or body.

Binding query parameters to a record

An endpoint with many optional filters would need a long list of @RequestParams. @ModelAttribute binds them to one object instead - here the BookFilter record, whose component names match the parameter names. (The annotation can even be left out for such a parameter.) -  OUTPUT of requests bound to a BookFilter record
> curl -i http://localhost:8080/api/books?author=Bloch&fromYear=2010
HTTP/1.1 200
Content-Length: 48
Content-Type: application/json

{"author":"Bloch","fromYear":2010,"toYear":null}

> curl -i http://localhost:8080/api/books?toYear=2005&fromYear=1995
HTTP/1.1 200
Content-Length: 45
Content-Type: application/json

{"author":null,"fromYear":1995,"toYear":2005}
Spring called the record's constructor with each parameter that was present, converted to Integer, and null for the others. The order of the parameters in the URL does not matter. Filtering and paging are covered in Pagination, Sorting and FilteringClick to read tutorial: Pagination, Sorting and Filtering in Spring Boot REST APIs.

The request body

@RequestBody converts the request body into an object with the JSON message converter - here an Order record containing a list of OrderLine records. consumes = "application/json" limits the mapping to JSON requests -  OUTPUT of a request with a JSON body
> curl -i -X POST http://localhost:8080/api/orders -H "Content-Type: application/json" -d '{"customer":"Nancy Smith","lines":[{"isbn":"9780134685991","quantity":2},{"isbn":"9781617293566","quantity":1}]}'
HTTP/1.1 201
Content-Length: 39
Content-Type: text/plain; charset=UTF-8

order for Nancy Smith: 2 lines, 3 items
The nested JSON became nested records, ready to use. A request has at most one body, so a method has at most one @RequestBody parameter; it can be combined freely with path variables, query parameters and headers.

When a request does not fit

Spring rejects requests that do not match a mapping, or whose data cannot be converted, with a precise status code - before any of your code runs -  OUTPUT of requests that do not fit any mapping
> curl -i http://localhost:8080/api/books/abc
HTTP/1.1 400
Content-Type: application/json

{"timestamp":"2026-11-06T09:18:16.481Z","status":400,"error":"Bad Request","path":"/api/books/abc"}

> curl -i -X DELETE http://localhost:8080/api/books/42
HTTP/1.1 405
Allow: GET
Content-Type: application/json

{"timestamp":"2026-11-06T09:18:16.601Z","status":405,"error":"Method Not Allowed","path":"/api/books/42"}

> curl -i http://localhost:8080/api/books/42/
HTTP/1.1 404
Content-Type: application/json

{"timestamp":"2026-11-06T09:18:16.622Z","status":404,"error":"Not Found","path":"/api/books/42/"}

> curl -i -X POST http://localhost:8080/api/orders -H "Content-Type: text/plain" -d 'Two copies of Effective Java please'
HTTP/1.1 415
Accept: application/json
Content-Type: application/json

{"timestamp":"2026-11-06T09:18:16.697Z","status":415,"error":"Unsupported Media Type","path":"/api/orders"}

> curl -i -X POST http://localhost:8080/api/orders -H "Content-Type: application/json" -d '{"customer":"Nancy Smith","lines":['
HTTP/1.1 400
Content-Type: application/json

{"timestamp":"2026-11-06T09:18:16.818Z","status":400,"error":"Bad Request","path":"/api/orders"}

Explanation of the output

  • /api/books/abc matched the {id} mapping, but abc cannot become a long: 400 Bad Request.
  • DELETE /api/books/42 matched a path that only supports GET: 405 Method Not Allowed, with an Allow header listing the methods that are supported.
  • /api/books/42/ with a trailing slash did not match: since Spring Framework 6, trailing slashes are no longer ignored by default, so /books/42 and /books/42/ are different paths.
  • A text/plain body sent to a mapping that consumes JSON: 415 Unsupported Media Type.
  • Malformed JSON could not be read: 400 Bad Request.
  • The bodies are Spring Boot's default error format; REST Error Handling with @ControllerAdvice and ProblemDetailClick to read tutorial: REST Error Handling with @ControllerAdvice and ProblemDetail shows how to return standard, more informative errors.

Summary

In this tutorial we first saw @RequestMapping, its method shortcuts and the attributes that narrow a mapping. We then captured path variables, including a regular expression and a catch-all {*path}, and read query parameters that were required, defaulted, optional, repeated, or collected into a map. Next we read headers and a cookie, bound several query parameters to a record with @ModelAttribute, and read a nested JSON body with @RequestBody. Lastly, we sent requests that did not fit and saw Spring answer 400, 404, 405 and 415. The next tutorial takes control of the response: ResponseEntity and HTTP status codesClick to read tutorial: ResponseEntity and HTTP Status Codes in Spring Boot.