Web / REST — Cheat Sheet
Spring Boot · 10 topics. Download the PDF or the Instagram carousel and share it.
@RestController & @Controller
@RestController combines @Controller and @ResponseBody, turning every handler method into a JSON/XML endpoint without needing explicit serialisation annotations.
- ✓@RestController = @Controller + @ResponseBody on every method.
- ✓Return values are serialised by HttpMessageConverters (Jackson for JSON by default).
- ✓@Controller returns view names; add @ResponseBody on individual methods for JSON.
- ✓Use ResponseEntity<T> for fine-grained control over status codes and headers.
- ✓@RestControllerAdvice centralises error handling across all controllers.
- ✓Content negotiation is driven by the Accept header — @RestController supports both JSON and XML with the right dependency.
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping
public List<ProductDTO> listAll() {
return productService.findAll(); // serialised to JSON array
}
@GetMapping("/{id}")
public ResponseEntity<ProductDTO> getById(@PathVariable Long id) {
return productService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ProductDTO create(@RequestBody @Valid CreateProductRequest req) {
return productService.create(req);
}
}Request Mapping & HTTP Methods
@GetMapping, @PostMapping, @PutMapping, @DeleteMapping, and @PatchMapping are shortcuts for @RequestMapping(method=…) that map HTTP verbs to handler methods.
- ✓@GetMapping, @PostMapping, etc. are composed annotations for cleaner code than @RequestMapping(method=...).
- ✓Class-level @RequestMapping sets a base path; method-level annotations append sub-paths.
- ✓consumes narrows by Content-Type; produces narrows by Accept header.
- ✓Path variables use {name} templates; regex constraints use {name:regex}.
- ✓Ant wildcards: * matches one segment, ** matches multiple segments.
- ✓Matrix variables (;key=val) need enableMatrixVariables = true in WebMvcConfigurer.
@RestController
@RequestMapping("/api/orders") // base path for all methods below
public class OrderController {
@GetMapping // GET /api/orders
public List<OrderDTO> list() { ... }
@GetMapping("/{id}") // GET /api/orders/{id}
public OrderDTO get(@PathVariable Long id) { ... }
@PostMapping // POST /api/orders
@ResponseStatus(HttpStatus.CREATED)
public OrderDTO create(@RequestBody @Valid CreateOrderRequest req) { ... }
@PutMapping("/{id}") // PUT /api/orders/{id}
public OrderDTO replace(@PathVariable Long id, @RequestBody OrderDTO dto) { ... }
@PatchMapping("/{id}/status") // PATCH /api/orders/{id}/status
public OrderDTO updateStatus(@PathVariable Long id,
@RequestBody StatusRequest req) { ... }
@DeleteMapping("/{id}") // DELETE /api/orders/{id}
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) { ... }
}@PathVariable & @RequestParam
@PathVariable extracts template variables from the URI path; @RequestParam binds query-string parameters, both support type conversion and defaulting.
- ✓@PathVariable binds {template} segments; name defaults to parameter name, override with value().
- ✓@RequestParam is required=true by default; use required=false or defaultValue for optional params.
- ✓Both support automatic type conversion: String → int, Long, enum, LocalDate, etc.
- ✓List<T> on @RequestParam captures repeated query params (?tags=a&tags=b).
- ✓Map<String, String> on @RequestParam captures all query params dynamically.
- ✓Add @Validated on the controller class to enable JSR-303 validation on @RequestParam values.
@RestController
@RequestMapping("/api/orders")
public class OrderController {
// Simple path variable
@GetMapping("/{id}")
public OrderDTO getById(@PathVariable Long id) {
return orderService.findById(id);
}
// Multiple path variables
@GetMapping("/{year}/{month}")
public List<OrderDTO> getByMonth(
@PathVariable int year,
@PathVariable int month) {
return orderService.findByMonth(year, month);
}
// Rename if variable name differs from template
@GetMapping("/{order-id}/items")
public List<ItemDTO> getItems(
@PathVariable("order-id") Long orderId) { // hyphen not valid Java identifier
return orderService.findItems(orderId);
}
// Regex constraint — only digits
@GetMapping("/{id:[0-9]+}/invoice")
public InvoiceDTO getInvoice(@PathVariable Long id) {
return orderService.getInvoice(id);
}
}@RequestBody & @ResponseBody
@RequestBody deserialises the HTTP body into a Java object using Jackson; @ResponseBody serialises the return value back to JSON or XML.
- ✓@RequestBody deserialises the HTTP body via HttpMessageConverter (Jackson for JSON).
- ✓@ResponseBody writes the return value directly to the HTTP response — implicit in @RestController.
- ✓Add @Valid to @RequestBody to trigger Bean Validation; MethodArgumentNotValidException on failure.
- ✓spring.jackson.deserialization.fail-on-unknown-properties=false prevents errors on extra fields.
- ✓Use JavaTimeModule to serialise/deserialise java.time types (LocalDateTime, ZonedDateTime).
- ✓Return StreamingResponseBody or Flux for large or real-time responses without memory buffering.
// Request DTO with validation constraints
public record CreateOrderRequest(
@NotNull Long customerId,
@NotEmpty List<@Valid OrderLineRequest> lines,
@Size(max = 500) String notes
) {}
public record OrderLineRequest(
@NotBlank String sku,
@Min(1) @Max(100) int quantity
) {}
// Controller
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public OrderDTO create(@RequestBody @Valid CreateOrderRequest req) {
// If any @NotNull / @NotEmpty / @Min fails, Spring throws
// MethodArgumentNotValidException before this method body runs
return orderService.create(req);
}
}
// Handle validation errors globally
@RestControllerAdvice
public class ValidationHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> handleValidation(MethodArgumentNotValidException ex) {
return ex.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(
FieldError::getField,
FieldError::getDefaultMessage
));
}
}ResponseEntity
Wraps the response body, HTTP status code, and headers in a single return type, giving full control over every aspect of the HTTP response.
- ✓ResponseEntity.ok(body) = 200, .created(uri) = 201 + Location, .noContent() = 204, .notFound() = 404
- ✓Always return 201 Created with a Location header pointing to the new resource URI after POST
- ✓ResponseEntity.status(code).header(name, value).body(obj) provides full control over every response aspect
- ✓@ControllerAdvice + @ExceptionHandler centralises error-to-ResponseEntity mapping across all controllers
- ✓ProblemDetail (Spring Boot 3 / RFC 7807) provides a standardised JSON error structure with type, title, status, detail
- ✓Return ResponseEntity<Void> for 204 No Content — avoids Jackson trying to serialise a null body
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderService orderService;
// 200 OK with body
@GetMapping("/{id}")
public ResponseEntity<Order> getOrder(@PathVariable Long id) {
return orderService.findById(id)
.map(ResponseEntity::ok) // 200 OK
.orElse(ResponseEntity.notFound().build()); // 404 Not Found, no body
}
// 201 Created with Location header
@PostMapping
public ResponseEntity<Order> createOrder(@RequestBody @Valid OrderRequest req) {
Order created = orderService.create(req);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(created.getId())
.toUri();
return ResponseEntity.created(location).body(created);
// Response: 201 Created, Location: /orders/42, body: {"id":42,...}
}
// 204 No Content — update/delete returns nothing
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteOrder(@PathVariable Long id) {
orderService.delete(id);
return ResponseEntity.noContent().build(); // 204, no body
}
}Exception Handling — @ControllerAdvice
@ControllerAdvice combined with @ExceptionHandler centralises error handling across all controllers, returning consistent error response DTOs.
- ✓@RestControllerAdvice centralises all exception-to-response mapping in one class, eliminating try-catch in every controller.
- ✓Return a consistent error DTO (status, error, message, timestamp) so all API consumers can parse errors uniformly.
- ✓MethodArgumentNotValidException carries per-field validation errors — extract and return field-level messages in a 400 response.
- ✓Define a custom exception hierarchy in the service layer; never reference HttpStatus there — keep HTTP concerns in the advice class.
- ✓Always have a catch-all @ExceptionHandler(Exception.class) as the last resort to prevent stack traces leaking to clients.
- ✓@ResponseStatus on the method is convenient; use ResponseEntity<ApiError> when you need to set response headers dynamically.
// Consistent error response DTO
@Getter
@Builder
public class ApiError {
private int status;
private String error;
private String message;
private Instant timestamp;
}
// Global exception handler
@RestControllerAdvice
public class GlobalExceptionHandler {
// 404 — Resource not found
@ExceptionHandler(EntityNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ApiError handleNotFound(EntityNotFoundException ex) {
return ApiError.builder()
.status(404)
.error("Not Found")
.message(ex.getMessage())
.timestamp(Instant.now())
.build();
}
// 409 — Business rule violation
@ExceptionHandler(DuplicateResourceException.class)
@ResponseStatus(HttpStatus.CONFLICT)
public ApiError handleConflict(DuplicateResourceException ex) {
return ApiError.builder()
.status(409).error("Conflict").message(ex.getMessage())
.timestamp(Instant.now()).build();
}
// 500 — Catch-all for unexpected errors
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ApiError handleAll(Exception ex) {
log.error("Unhandled exception", ex);
return ApiError.builder()
.status(500).error("Internal Server Error")
.message("An unexpected error occurred")
.timestamp(Instant.now()).build();
}
}Bean Validation with @Valid
JSR-303/380 constraints (@NotNull, @Size, @Email) on request DTOs are enforced automatically when @Valid is placed on the method parameter.
- ✓Add spring-boot-starter-validation to get Hibernate Validator on the classpath
- ✓@Valid on @RequestBody triggers validation; @Validated adds group support
- ✓@NotBlank is stricter than @NotNull — it also rejects empty/whitespace strings
- ✓Nested objects require their own @Valid annotation for cascaded validation
- ✓Handle MethodArgumentNotValidException in @ControllerAdvice for structured 400 responses
- ✓Custom constraints: create a @Constraint annotation + ConstraintValidator<A, T> class
public record CreateUserRequest(
@NotBlank(message = "Name is required")
@Size(max = 100, message = "Name must be at most 100 characters")
String name,
@NotNull @Email(message = "Must be a valid email address")
String email,
@NotNull @Min(18) @Max(120)
Integer age,
@Valid // cascade validation into the nested object
@NotNull
AddressRequest address
) {}
public record AddressRequest(
@NotBlank String street,
@Pattern(regexp = "^[0-9]{5}$", message = "ZIP must be 5 digits")
String zip
) {}CORS Configuration
Cross-Origin Resource Sharing is configured globally via WebMvcConfigurer or per-controller with @CrossOrigin to control which origins browsers allow.
- ✓CORS is enforced by browsers, not servers — server-to-server calls are never blocked by CORS.
- ✓Spring Security must be involved in CORS config; WebMvcConfigurer alone is bypassed when security is active.
- ✓Never use allowedOrigins("*") with allowCredentials(true) — browsers reject it and it is a security vulnerability.
- ✓Preflight OPTIONS requests must return 200 quickly; Spring handles them automatically when CORS is configured.
- ✓Use allowedOrigins with explicit domains in production; use environment-specific properties to avoid hardcoding.
- ✓exposedHeaders lists headers the browser JavaScript can read from the response — only listed headers are accessible.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins(
"https://app.example.com",
"https://staging.example.com"
)
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
.allowedHeaders("*")
.exposedHeaders("X-Correlation-Id", "Location")
.allowCredentials(true) // allows cookies/Authorization headers
.maxAge(3600); // cache preflight for 1 hour
// Public endpoints — allow all origins, no credentials
registry.addMapping("/public/**")
.allowedOrigins("*")
.allowedMethods("GET")
.allowCredentials(false);
}
}Filters & Interceptors
Servlet Filters operate at the servlet container level for all requests; Spring HandlerInterceptors run around MVC dispatch and have access to handler metadata.
- ✓Filters run at the servlet container level — before Spring MVC. Interceptors run inside DispatcherServlet — after handler resolution.
- ✓Interceptors have access to HandlerMethod — you can read method annotations and controller class for context-aware logic.
- ✓Spring Security uses Filters (not Interceptors) because security must run before MVC handler resolution.
- ✓Register filters with @Component (all paths) or FilterRegistrationBean (specific patterns + order control).
- ✓Register interceptors with WebMvcConfigurer.addInterceptors() and scope them with addPathPatterns().
- ✓Filters cannot access Spring beans directly unless using DelegatingFilterProxy; Interceptors are Spring-managed and can @Autowire anything.
// Custom filter — logs request/response timing
@Component
@Order(1) // lower number = higher priority
public class RequestLoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
long start = System.currentTimeMillis();
String requestId = UUID.randomUUID().toString().substring(0, 8);
req.setAttribute("requestId", requestId);
try {
chain.doFilter(request, response); // pass to next filter / servlet
} finally {
long duration = System.currentTimeMillis() - start;
log.info("[{}] {} {} → {} ({}ms)",
requestId, req.getMethod(), req.getRequestURI(),
resp.getStatus(), duration);
}
}
}
// Fine-grained registration — apply only to /api/**
@Bean
public FilterRegistrationBean<RequestLoggingFilter> loggingFilter() {
FilterRegistrationBean<RequestLoggingFilter> reg = new FilterRegistrationBean<>();
reg.setFilter(new RequestLoggingFilter());
reg.addUrlPatterns("/api/*");
reg.setOrder(1);
return reg;
}Content Negotiation
Spring MVC selects the response format (JSON, XML, etc.) based on Accept headers or URL suffixes, driven by registered HttpMessageConverters.
- ✓Spring picks the HttpMessageConverter based on the Accept header (response) or Content-Type header (request body)
- ✓produces = MediaType.APPLICATION_JSON_VALUE on a method restricts the endpoint to JSON output only — returns 406 otherwise
- ✓consumes = MediaType.APPLICATION_JSON_VALUE restricts accepted request body type — returns 415 for other types
- ✓JAXB2 XML support requires @XmlRootElement on the entity and jackson-dataformat-xml or JAXB2 on the classpath
- ✓@JsonView selects field subsets per endpoint, avoiding separate DTO classes for summary vs detail views
- ✓Custom HttpMessageConverters can support any media type (CSV, protobuf, YAML) by extending AbstractHttpMessageConverter
// Client sends: Accept: application/xml
// Spring finds Jaxb2 converter → returns XML
@RestController
@RequestMapping("/orders")
public class OrderController {
// Endpoint produces both JSON and XML — client chooses via Accept header
@GetMapping(value = "/{id}",
produces = {MediaType.APPLICATION_JSON_VALUE,
MediaType.APPLICATION_XML_VALUE})
public Order getOrder(@PathVariable Long id) {
return orderService.findById(id).orElseThrow();
// GET /orders/1 Accept: application/json → {"id":1,...}
// GET /orders/1 Accept: application/xml → <Order><id>1</id>...</Order>
}
// Restrict endpoint to only accept JSON request body
@PostMapping(value = "/",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Order> createOrder(@RequestBody OrderRequest req) {
// Will return 415 Unsupported Media Type if client sends XML
return ResponseEntity.ok(orderService.create(req));
}
}
// XML support requires JAXB annotations on the entity
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Order { ... }