1. Basic REST Controller
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService service;
@GetMapping
public List<UserDto> getAll() {
return service.allUsers();
}
@GetMapping("/{id}")
public UserDto getById(@PathVariable Long id) {
return service.getUser(id);
}
@PostMapping
public UserDto create(@Valid @RequestBody UserDto dto) {
return service.create(dto);
}
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id) {
service.delete(id);
}
}📌 Annotations:
- @RestController = @Controller + @ResponseBody
- @RequestMapping → base prefix (/api/users)
- @GetMapping, @PostMapping, @PutMapping, @DeleteMapping
2. Validation (JSR-380 Bean Validation)
📄 DTO:
public record UserDto(
@NotBlank String name,
@Email String email,
@Min(18) int age
) {}📄 In the controller:
@PostMapping
public UserDto create(@Valid @RequestBody UserDto dto) {
return service.create(dto);
}📄 Error Handling:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidation(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors()
.forEach(e -> errors.put(e.getField(), e.getDefaultMessage()));
return ResponseEntity.badRequest().body(errors);
}
}3. Error Handling (ProblemDetail — Spring 6, RFC 7807)
@RestControllerAdvice
class ApiExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
ProblemDetail handleIllegalArgument(IllegalArgumentException e) {
ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
pd.setTitle("Validation failed");
pd.setDetail(e.getMessage());
return pd;
}
}📌 The JSON response will be in the format:
{
"type": "about:blank",
"title": "Validation failed",
"status": 400,
"detail": "Invalid email"
}4. Pagination and Sorting
Spring Data JPA handles this automatically:
@GetMapping
public Page<UserDto> all(@RequestParam int page, @RequestParam int size) {
return repo.findAll(PageRequest.of(page, size, Sort.by("name").ascending()));
}📌 The JSON response will have the fields:
{
"content": [...],
"totalPages": 5,
"totalElements": 100,
"number": 0,
"size": 20
}5. Filtering (Specification API)
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {}
@GetMapping("/search")
public List<User> search(@RequestParam(required=false) String name,
@RequestParam(required=false) Integer minAge) {
return repo.findAll((root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (name != null) predicates.add(cb.like(root.get("name"), "%" + name + "%"));
if (minAge != null) predicates.add(cb.greaterThanOrEqualTo(root.get("age"), minAge));
return cb.and(predicates.toArray(new Predicate[0]));
});
}6. Uploading, Downloading Files
Upload
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) throws IOException {
Path path = Paths.get("uploads/" + file.getOriginalFilename());
Files.copy(file.getInputStream(), path, StandardCopyOption.REPLACE_EXISTING);
return "Uploaded: " + path;
}Download
@GetMapping("/download/{filename}")
public ResponseEntity<Resource> download(@PathVariable String filename) throws IOException {
Path path = Paths.get("uploads/" + filename);
Resource resource = new UrlResource(path.toUri());
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename)
.body(resource);
}7. Rate Limiting (Request Limiting)
📄 Simple option (Resilience4j + Spring Boot Starter):
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
</dependency>📄 Config:
resilience4j.ratelimiter:
instances:
apiLimiter:
limitForPeriod: 5
limitRefreshPeriod: 10s
timeoutDuration: 0📄 Usage:
@RestController
@RequiredArgsConstructor
public class DemoController {
@RateLimiter(name = "apiLimiter")
@GetMapping("/limited")
public String limited() {
return "OK";
}
}8. Documentation (Swagger, OpenAPI)
📄 Adding dependency:
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.5.0</version>
</dependency>📌 Now available:
- Swagger UI → http://localhost:8080/swagger-ui.html
- OpenAPI JSON → http://localhost:8080/v3/api-docs
9. CORS (Cross-Origin Resource Sharing)
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("GET","POST","PUT","DELETE");
}
};
}
}10. Best Practices
- ✅ Use DTO + @Valid instead of Entity directly.
- ✅ Handle errors through @RestControllerAdvice.
- ✅ Always add pagination/sorting to GET requests.
- ✅ For APIs, it is better to use ProblemDetail (RFC 7807).
- ✅ Document the API through Swagger/OpenAPI.
- ✅ Add rate limiting for protection.