Spring: Spring MVC & REST APIs
REST Controller
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
@Validated
public class UserController {
private final UserService userService;
@GetMapping
public ResponseEntity<Page<UserDto>> getAll(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "25") @Max(100) int size,
@RequestParam(required = false) String search) {
return ResponseEntity.ok(userService.getAll(page, size, search));
}
@GetMapping("/{id}")
public ResponseEntity<UserDto> getById(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public UserDto create(@Valid @RequestBody CreateUserRequest request) {
return userService.create(request);
}
@PatchMapping("/{id}")
public UserDto update(@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
return userService.update(id, request);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
userService.delete(id);
}
}Request & Response DTOs
// Record DTOs (Java 16+)
public record UserDto(Long id, String name, String email, LocalDateTime createdAt) {}
public record CreateUserRequest(
@NotBlank @Size(max = 100) String name,
@NotBlank @Email String email,
@NotBlank @Size(min = 8) String password
) {}
// Custom validation annotation
@Constraint(validatedBy = UniqueEmailValidator.class)
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UniqueEmail {
String message() default "Email already registered";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// ControllerAdvice — global exception handling
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
public Map<String, Object> handleValidation(MethodArgumentNotValidException ex) {
var errors = ex.getBindingResult().getFieldErrors().stream()
.map(e -> Map.of("field", e.getField(), "message", e.getDefaultMessage()))
.toList();
return Map.of("status", 422, "errors", errors);
}
@ExceptionHandler(EntityNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Map<String, String> handleNotFound(EntityNotFoundException ex) {
return Map.of("error", ex.getMessage());
}
}Filters, Interceptors & CORS
// CORS configuration
@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
var config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://app.example.com"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
config.setAllowCredentials(true);
config.setMaxAge(86400L);
var source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
// HandlerInterceptor — runs around controller methods
@Component
public class RequestLoggingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
req.setAttribute("startTime", System.currentTimeMillis());
return true; // true = continue chain
}
@Override
public void afterCompletion(HttpServletRequest req, HttpServletResponse res,
Object handler, Exception ex) {
long duration = System.currentTimeMillis() - (long) req.getAttribute("startTime");
log.info("{} {} → {} in {}ms", req.getMethod(), req.getRequestURI(),
res.getStatus(), duration);
}
}Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free