Spring Web MVC is the servlet-based web framework underneath every @RestController you
write. You can be productive without knowing how a request reaches your method — until something goes
wrong in the gap between the socket and your code, at which point knowing the path is the difference
between a fix and a guess.
One dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>⚠️ In Boot 3 this was spring-boot-starter-web. Boot 4 renamed it and the old name is
not in the BOM — see lesson 3.
That starter brings Spring MVC, Jackson, an embedded Tomcat and validation glue, and
auto-configures a DispatcherServlet mapped to /.
How a request reaches your method
HTTP request
│
▼
Servlet container (embedded Tomcat)
│
▼
Filter chain ← JwtAuthenticationFilter, CORS, Spring Security live here
│
▼
DispatcherServlet ← the front controller: one servlet for every request
│
├─▶ HandlerMapping which method handles this URL and verb?
├─▶ HandlerAdapter invoke it
│ ├─▶ ArgumentResolvers build the method's parameters
│ │ @PathVariable · @RequestParam · @RequestBody · @RequestPart
│ │
│ └─▶ your controller method
│
├─▶ HttpMessageConverter turn the return value into a response body
└─▶ HandlerExceptionResolver if anything threw → @RestControllerAdviceThree things are worth internalising from that diagram:
- Filters run before the DispatcherServlet. Spring Security is a filter, so a 401 or 403 can be produced before your controller is ever consulted — which is why an endpoint can return 403 for a URL that has no handler at all.
- Argument resolvers build your parameters.
@RequestBodyis not magic; it is a resolver calling a message converter. - Message converters produce the body. Returning a
ProductDTOworks because Jackson is on the classpath and a converter knows what to do with it.
Mapping requests
@Tag(name = "Products", description = "Menu browsing (public)")
@RequestMapping("/api/products")
@RestController
@Slf4j
public class ProductRestController {
@Autowired
private ProductService productService;
@Operation(summary = "List the active menu, optionally filtered by type")
@GetMapping
public ResponseEntity<List<ProductDTO>> getProducts(
@RequestParam(required = false) ProductType type) {
// …
}
@Operation(summary = "Get one product with its sizes")
@GetMapping("/{id}")
public ResponseEntity<ProductDTO> getProduct(@PathVariable UUID id) {
log.info("GET /api/products/{}", id);
return new ResponseEntity<>(productService.getProductByPublicId(id), OK);
}
}@RequestMapping on the class sets the prefix; @GetMapping,
@PostMapping, @PutMapping, @PatchMapping and
@DeleteMapping are shorthands for it with a method.
Notice the parameter types: UUID and ProductType, not
String. Spring converts, and a value that will not convert becomes a
MethodArgumentTypeMismatchException before your method runs — so the method body never
has to consider malformed input. Lesson 12 turns that into a clean 400.
Binding parameters
// Path segment
@GetMapping("/{id}")
public ProductDTO get(@PathVariable UUID id) { }
// Query string: /api/products?type=DRINK
@GetMapping
public List<ProductDTO> list(@RequestParam(required = false) ProductType type) { }
// With a default
@GetMapping("/search")
public List<ProductDTO> search(
@RequestParam String q,
@RequestParam(defaultValue = "0") int page) { }
// JSON body, validated
@PostMapping
public ProductDTO create(@Valid @RequestBody ProductCreateDTO dto) { }
// One named part of a multipart body
@PostMapping(value = "/{id}/image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ProductDTO upload(@PathVariable UUID id, @RequestPart("file") MultipartFile file) { }
// A header
@PostMapping("/stripe")
public ResponseEntity<String> webhook(
@RequestBody String payload,
@RequestHeader(value = "Stripe-Signature", required = false) String signature) { }required = false versus defaultValue: the first gives you
null and a decision to make, the second gives you a value. Prefer
defaultValue when there is a sensible one — it removes a null check from your
method.
Message converters
Return an object and Jackson serialises it. Return ResponseEntity<T> and you
also control the status and headers:
return ResponseEntity.ok()
.contentType(MediaType.IMAGE_JPEG)
.cacheControl(CacheControl.maxAge(Duration.ofDays(365)).cachePublic())
.header("Content-Disposition", "inline; filename=\"" + fileName + "\"")
.body(image);Converters are chosen by the return type and the request's Accept header. Returning a
Resource rather than a byte[] matters for files: Spring streams it, so a
2 MB image never sits in heap in its entirety. With byte[] the whole file is buffered
per concurrent request.
CORS
A browser refuses cross-origin requests unless the server opts in. The pizza API's React app runs
on :5173 and the API on :8085 — different origins, so CORS is required.
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(properties.cors().allowedOrigins());
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}⚠️ CORS and Spring Security have to agree. Security runs as a filter, ahead of
MVC, so it can reject the browser's preflight OPTIONS request before any CORS handling
happens — and the browser then reports a CORS error for what is really an authentication problem.
The fix is to let Security use the same configuration:
http.cors(Customizer.withDefaults()) // picks up the CorsConfigurationSource bean aboveAlso note allowCredentials(true) is incompatible with
allowedOrigins("*"). The spec forbids it, and the error message is about the wildcard
rather than about credentials.
Static resources
Anything in src/main/resources/static/ is served from the classpath automatically.
Add your own mappings when you need to:
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}What to take from this
- Boot 4:
spring-boot-starter-webmvc. - Filters run before the DispatcherServlet, which is why security errors can appear for URLs that have no handler.
- Bind to real types —
UUID, enums — and let conversion failures become 400s rather than method-body checks. - Make CORS and Security use the same configuration, or preflight failures will masquerade as CORS bugs.
Next: building a REST API — status codes, DTOs, and why the pizza API exposes a UUID instead of its primary key.