Building REST APIs with Spring Boot

Request Mapping

18 min Lesson 2 of 13

Request Mapping

In the previous lesson you learned that @RestController marks a class as the entry point for HTTP traffic. But a controller on its own does nothing — you need to tell Spring which HTTP method and URL path should trigger each handler method. That is what request mapping annotations do, and choosing them correctly is one of the most important design decisions you make when building a REST API.

The Four Core HTTP Verbs

REST maps CRUD operations onto four HTTP methods, each with a distinct semantic contract:

  • GET — retrieve a resource; must be safe (no side effects) and idempotent.
  • POST — create a new resource, or trigger an action; not idempotent.
  • PUT — replace a resource entirely; idempotent (calling it twice gives the same result).
  • DELETE — remove a resource; idempotent.

Spring provides a dedicated shortcut annotation for each one. The older, generic @RequestMapping still works and is useful when you need to share a base path across all methods in a controller, but for individual handlers the shortcuts are cleaner and more readable.

@GetMapping

Use @GetMapping for any operation that reads data without modifying server state. A well-designed GET handler can be cached, retried safely, and called repeatedly with no concern about duplicate side effects.

import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/products") // base path shared by all methods in this controller public class ProductController { private final ProductService productService; public ProductController(ProductService productService) { this.productService = productService; } // GET /api/products — list all products @GetMapping public List<Product> listAll() { return productService.findAll(); } // GET /api/products/{id} — fetch a single product @GetMapping("/{id}") public Product getOne(@PathVariable Long id) { return productService.findById(id); } }
Class-level @RequestMapping is a prefix. When you place @RequestMapping("/api/products") on the class, every method-level annotation appends to it. @GetMapping("/{id}") resolves to GET /api/products/{id}. This avoids repeating the base path on every method.

@PostMapping

POST is the right verb when you are asking the server to create something new. The canonical pattern is: the client sends a JSON body with the resource data, the server creates the resource, and the response contains the newly created resource (or at minimum its ID and a 201 Created status).

// POST /api/products @PostMapping @ResponseStatus(HttpStatus.CREATED) // return 201 instead of the default 200 public Product create(@RequestBody ProductRequest request) { return productService.create(request); }

The @RequestBody annotation tells Spring to deserialize the incoming JSON into a Java object using Jackson. You will go deeper into Jackson and ResponseEntity in later lessons; for now, note that POST should return 201 Created, not 200 OK.

@PutMapping

PUT replaces the target resource completely. If the client sends a partial object, the server should treat missing fields as intentional nulls (or the resource's empty state). If you want partial updates, use PATCH instead — but that is covered in the best-practices lesson.

// PUT /api/products/{id} @PutMapping("/{id}") public Product replace(@PathVariable Long id, @RequestBody ProductRequest request) { return productService.replace(id, request); }
PUT vs POST for creation. Some APIs allow PUT /resources/{id} to create a resource if it does not exist (upsert). This is valid REST but requires the client to supply the ID. It is a good fit when IDs are natural keys (e.g. a username). When IDs are server-generated (e.g. auto-increment database keys), POST-to-create is the standard pattern.

@DeleteMapping

DELETE removes the identified resource. A successful delete conventionally returns 204 No Content — the operation succeeded but there is no body to return.

// DELETE /api/products/{id} @DeleteMapping("/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) // 204 public void delete(@PathVariable Long id) { productService.delete(id); }

Path Design — Practical Rules

The URL structure you choose is part of your API's public contract. Changing it later is a breaking change for every client. These rules save pain:

  1. Use nouns, not verbs. The HTTP method is the verb. Write /api/products, not /api/getProducts or /api/createProduct.
  2. Use plural nouns for collections. /api/products for the collection, /api/products/{id} for a member. Mixing singular and plural confuses callers.
  3. Reflect relationships in the path. If a review belongs to a product, the path /api/products/{productId}/reviews makes the ownership explicit.
  4. Keep paths lowercase with hyphens. /api/product-categories, not /api/productCategories or /api/ProductCategories. URLs are case-sensitive on most servers, and camelCase looks out of place.
  5. Do not version in the path yet. Versioning is lesson 9; adding /v1/ on day one without a strategy creates clutter.
// Good — resource-oriented, plural nouns, lowercase @RestController @RequestMapping("/api/orders") public class OrderController { @GetMapping // GET /api/orders public List<Order> list() { ... } @GetMapping("/{orderId}") // GET /api/orders/{orderId} public Order get(@PathVariable Long orderId) { ... } @GetMapping("/{orderId}/items") // GET /api/orders/{orderId}/items — nested resource public List<OrderItem> items(@PathVariable Long orderId) { ... } @PostMapping // POST /api/orders @ResponseStatus(HttpStatus.CREATED) public Order place(@RequestBody OrderRequest req) { ... } @PutMapping("/{orderId}") // PUT /api/orders/{orderId} public Order update(@PathVariable Long orderId, @RequestBody OrderRequest req) { ... } @DeleteMapping("/{orderId}") // DELETE /api/orders/{orderId} @ResponseStatus(HttpStatus.NO_CONTENT) public void cancel(@PathVariable Long orderId) { ... } }

Under the Hood: How Annotations Map to @RequestMapping

The shortcut annotations are thin wrappers. For example, @GetMapping("/path") is exactly equivalent to @RequestMapping(value = "/path", method = RequestMethod.GET). Knowing this matters when you need to set additional attributes like produces (content negotiation) or consumes — you can use the shortcut and add those attributes directly:

@GetMapping(value = "/{id}", produces = "application/json") public Product getOne(@PathVariable Long id) { ... } @PostMapping(value = "", consumes = "application/json", produces = "application/json") @ResponseStatus(HttpStatus.CREATED) public Product create(@RequestBody ProductRequest req) { ... }
Ambiguous mappings cause startup failure. If two handler methods in the same application resolve to the same HTTP method + path combination, Spring throws IllegalStateException: Ambiguous mapping at startup. This is a good thing — it is a compile-time (well, startup-time) contract violation, not a silent runtime bug. Fix it by differentiating paths, methods, or consumes/produces constraints.

Summary

@GetMapping, @PostMapping, @PutMapping, and @DeleteMapping pin your handler methods to specific HTTP verbs and paths. Combine them with a class-level @RequestMapping prefix to keep your controller clean. Design your paths around resource nouns, use plural names for collections, and reflect ownership in nested sub-paths. In the next lesson you will see how to extract dynamic segments from those paths using @PathVariable and @RequestParam.