Spring BootRequest Mapping
Spring Boot

Request Mapping

Request mapping is how Spring Boot routes incoming HTTP requests to controller handler methods. @RequestMapping and its method-specific shortcuts (@GetMapping, @PostMapping, @PutMapping, @PatchMapping, @DeleteMapping) define which URL patterns, HTTP methods, content types, and headers a handler accepts. Understanding the full mapping model — including wildcards, patterns, and condition narrowing — is essential for building predictable REST APIs.

The @RequestMapping Annotation

@RequestMapping is the foundational routing annotation in Spring MVC. It can be placed on a class (sets a base path for all methods in the controller) and on individual handler methods (refines the mapping). Every method-level shortcut annotation — @GetMapping, @PostMapping, etc. — is a composed annotation that wraps @RequestMapping with a fixed method attribute.
Java
// ── Class-level @RequestMapping — sets the base path ─────────────────
@RestController
@RequestMapping("/users")   // all handler methods in this class share /users
public class UserController {

    // Inherits /users — handles GET /users
    @RequestMapping(method = RequestMethod.GET)
    public List<UserResponse> findAll() { ... }

    // Inherits /users — handles POST /users
    @RequestMapping(method = RequestMethod.POST)
    public ResponseEntity<UserResponse> create(...) { ... }

    // Inherits /users — handles GET /users/{id}
    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public UserResponse findById(@PathVariable Long id) { ... }
}

// ── @RequestMapping attributes ────────────────────────────────────────
@RequestMapping(
    value    = "/users/{id}",          // URI pattern (also: path = "...")
    method   = RequestMethod.PUT,      // HTTP method(s)
    consumes = "application/json",     // required Content-Type of request body
    produces = "application/json",     // response Content-Type (matches Accept header)
    headers  = "X-API-Version=2",      // required request header
    params   = "version=2"             // required query parameter
)
public ResponseEntity<UserResponse> update(...) { ... }

// ── Multiple values per attribute ─────────────────────────────────────
@RequestMapping(
    value   = {"/users", "/members"},  // matches either URI
    method  = {RequestMethod.GET, RequestMethod.HEAD},
    produces = {"application/json", "application/xml"}
)
public List<UserResponse> findAll() { ... }

Method-Specific Shortcut Annotations

Spring provides composed annotations for each HTTP method. These are the standard choice for REST controllers — they are more concise than @RequestMapping and make the HTTP method explicit at a glance.
Java
@RestController
@RequestMapping("/users")
public class UserController {

    // GET /users
    @GetMapping
    public Page<UserResponse> findAll(Pageable pageable) { ... }

    // GET /users/{id}
    @GetMapping("/{id}")
    public UserResponse findById(@PathVariable Long id) { ... }

    // POST /users
    @PostMapping
    public ResponseEntity<UserResponse> create(
            @RequestBody @Valid CreateUserRequest request) { ... }

    // PUT /users/{id}
    @PutMapping("/{id}")
    public UserResponse update(
            @PathVariable Long id,
            @RequestBody @Valid UpdateUserRequest request) { ... }

    // PATCH /users/{id}
    @PatchMapping("/{id}")
    public UserResponse patch(
            @PathVariable Long id,
            @RequestBody @Valid PatchUserRequest request) { ... }

    // DELETE /users/{id}
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) { ... }
}

// Each shortcut is a meta-annotation — @GetMapping is defined as:
// @RequestMapping(method = RequestMethod.GET)
// They accept all the same attributes as @RequestMapping
// (value, consumes, produces, headers, params) except method.

URI Patterns and Wildcards

Spring MVC supports several URI pattern syntaxes for flexible route matching. Literal paths, path variables, wildcards, and regex segments can be combined to express complex routing rules.
Java
@RestController
public class PatternController {

    // ── Literal path ──────────────────────────────────────────────────
    @GetMapping("/users")
    public List<UserResponse> findAll() { ... }

    // ── Path variable — matches any single segment ─────────────────────
    @GetMapping("/users/{id}")
    public UserResponse findById(@PathVariable Long id) { ... }

    // ── Multiple path variables ────────────────────────────────────────
    @GetMapping("/users/{userId}/orders/{orderId}")
    public OrderResponse findOrder(
            @PathVariable Long userId,
            @PathVariable Long orderId) { ... }

    // ── Single wildcard * — matches any characters within one segment ──
    @GetMapping("/files/*.png")     // matches /files/photo.png, /files/logo.png
    public Resource getPng(...) { ... }

    // ── Double wildcard ** — matches any number of path segments ───────
    @GetMapping("/files/**")        // matches /files/a, /files/a/b/c
    public Resource getFile(...) { ... }

    // ── Regex constraint on a path variable ───────────────────────────
    // {variable:regex} — only matches when the segment satisfies the regex:
    @GetMapping("/users/{id:[0-9]+}")         // only numeric ids
    public UserResponse findById(@PathVariable Long id) { ... }

    @GetMapping("/products/{slug:[a-z0-9-]+}") // lowercase slug only
    public ProductResponse findBySlug(@PathVariable String slug) { ... }

    // ── Optional trailing slash (Spring Boot 3 default: not matched) ───
    // Spring Boot 3+ removed trailing slash matching by default.
    // /users and /users/ are distinct — map both explicitly if needed:
    @GetMapping({"/users", "/users/"})
    public List<UserResponse> findAllWithSlash() { ... }
}

consumes and produces

The consumes attribute narrows a mapping to requests whose Content-Type matches. The produces attribute narrows to requests whose Accept header is compatible. Both support negation and multiple values, and both drive Spring's content negotiation.
Java
@RestController
@RequestMapping("/users")
public class UserController {

    // ── produces — handler only matches when client accepts JSON ───────
    @GetMapping(value = "/{id}", produces = "application/json")
    public UserResponse findByIdJson(@PathVariable Long id) { ... }

    // ── produces — separate handler for XML ───────────────────────────
    @GetMapping(value = "/{id}", produces = "application/xml")
    public UserResponse findByIdXml(@PathVariable Long id) { ... }

    // ── consumes — only accepts JSON request bodies ────────────────────
    @PostMapping(consumes = "application/json")
    public ResponseEntity<UserResponse> createFromJson(
            @RequestBody CreateUserRequest request) { ... }

    // ── consumes — separate handler for form submissions ──────────────
    @PostMapping(consumes = "application/x-www-form-urlencoded")
    public ResponseEntity<UserResponse> createFromForm(
            @ModelAttribute CreateUserRequest request) { ... }

    // ── Multiple media types ───────────────────────────────────────────
    @PostMapping(
        consumes = {"application/json", "application/xml"},
        produces = {"application/json", "application/xml"}
    )
    public ResponseEntity<UserResponse> createAny(
            @RequestBody CreateUserRequest request) { ... }

    // ── Negation — match anything except JSON ─────────────────────────
    @PostMapping(consumes = "!application/json")
    public ResponseEntity<String> rejectNonJson() {
        return ResponseEntity.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE).build();
    }
}

// If no handler matches the client's Accept header: 406 Not Acceptable
// If no handler matches the request's Content-Type: 415 Unsupported Media Type

headers and params Narrowing

The headers and params attributes further narrow which requests a handler accepts. Both support presence checks, value checks, and negation. These are less common in REST APIs but useful for versioning and legacy compatibility.
Java
@RestController
@RequestMapping("/users")
public class UserController {

    // ── headers — match only when header is present ────────────────────
    @GetMapping(headers = "X-Internal-Request")
    public List<UserResponse> findAllInternal() { ... }

    // ── headers — match specific header value ─────────────────────────
    @GetMapping(headers = "X-API-Version=2")
    public List<UserResponseV2> findAllV2() { ... }

    @GetMapping(headers = "X-API-Version=1")
    public List<UserResponseV1> findAllV1() { ... }

    // ── headers — negation (header must NOT be present) ────────────────
    @GetMapping(headers = "!X-Internal-Request")
    public List<UserResponse> findAllPublic() { ... }

    // ── params — match only when query parameter is present ───────────
    @GetMapping(params = "format")
    public List<UserResponse> findAllWithFormat(
            @RequestParam String format) { ... }

    // ── params — match specific parameter value ────────────────────────
    @GetMapping(params = "version=2")
    public List<UserResponseV2> findAllParamV2() { ... }

    // ── params — negation ─────────────────────────────────────────────
    @GetMapping(params = "!debug")
    public List<UserResponse> findAllNonDebug() { ... }

    // ── Combining multiple conditions ──────────────────────────────────
    @PostMapping(
        consumes = "application/json",
        headers  = "X-API-Version=2",
        params   = "strict=true"
    )
    public ResponseEntity<UserResponse> createStrictV2(
            @RequestBody @Valid CreateUserRequest request) { ... }
}

Controller-Level vs Method-Level Mapping

Class-level @RequestMapping sets a base path inherited by all methods. Method-level annotations append to it. Understanding how they compose prevents routing surprises — especially with leading slashes and empty paths.
Java
// ── Base path composition ─────────────────────────────────────────────
@RestController
@RequestMapping("/api/v1/users")    // base path
public class UserController {

    @GetMapping                     // → GET /api/v1/users
    public List<UserResponse> findAll() { ... }

    @GetMapping("/{id}")            // → GET /api/v1/users/{id}
    public UserResponse findById(@PathVariable Long id) { ... }

    @GetMapping("/{id}/orders")     // → GET /api/v1/users/{id}/orders
    public List<OrderResponse> findOrders(@PathVariable Long id) { ... }
}

// ── Versioned controllers — one class per version ──────────────────────
@RestController
@RequestMapping("/api/v1/products")
public class ProductControllerV1 {
    @GetMapping("/{id}")            // → GET /api/v1/products/{id}
    public ProductResponseV1 findById(@PathVariable Long id) { ... }
}

@RestController
@RequestMapping("/api/v2/products")
public class ProductControllerV2 {
    @GetMapping("/{id}")            // → GET /api/v2/products/{id}
    public ProductResponseV2 findById(@PathVariable Long id) { ... }
}

// ── Class-level produces/consumes — inherited by all methods ──────────
@RestController
@RequestMapping(value = "/users", produces = "application/json")
public class UserController {

    @GetMapping           // inherits produces = "application/json"
    public List<UserResponse> findAll() { ... }

    @GetMapping("/{id}")  // inherits produces = "application/json"
    public UserResponse findById(@PathVariable Long id) { ... }

    @GetMapping(value = "/{id}/avatar", produces = "image/png")
    public byte[] getAvatar(@PathVariable Long id) { ... }
    // Method-level produces overrides the class-level value for this method only
}

Ambiguous Mapping and Handler Resolution

When multiple handlers match the same request, Spring applies specificity rules to pick the best match. Understanding the resolution order helps you design mappings that behave predictably and avoid AmbiguousRequestMappingException.
Java
// Spring resolves ambiguous mappings by specificity — more specific wins:
// 1. Exact literal path  >  path with variables  >  path with wildcards
// 2. More conditions (consumes + produces + headers) > fewer conditions
// 3. More specific media type (application/json) > wildcard (*/*)

// ── Example: literal beats variable ───────────────────────────────────
@GetMapping("/users/me")         // matches GET /users/me (literal — wins)
public UserResponse getMe() { ... }

@GetMapping("/users/{id}")       // matches GET /users/42 (variable)
public UserResponse findById(@PathVariable Long id) { ... }
// GET /users/me → handled by getMe(), not findById("me")

// ── Example: consumes narrows the match ───────────────────────────────
@PostMapping("/users")
public ResponseEntity<UserResponse> createDefault(
        @RequestBody CreateUserRequest req) { ... }

@PostMapping(value = "/users", consumes = "application/json")
public ResponseEntity<UserResponse> createJson(
        @RequestBody CreateUserRequest req) { ... }
// POST /users with Content-Type: application/json → createJson() wins (more specific)
// POST /users with Content-Type: application/xml  → createDefault() handles it

// ── AmbiguousRequestMappingException ─────────────────────────────────
// Two handlers with identical mapping conditions → startup failure:
@GetMapping("/users/{id}")   // DUPLICATE
public UserResponse handlerA(@PathVariable Long id) { ... }

@GetMapping("/users/{id}")   // DUPLICATE — context fails to start
public UserResponse handlerB(@PathVariable Long id) { ... }

// Fix: differentiate by consumes, produces, headers, or params,
// or remove one of the handlers.