Date: December 24, 2024
Welcome back to our Spring Boot 3 and Java 17 tutorial series! In Part 1, we set up our project and created a simple REST endpoint. Now, we’re going to take a deeper dive into designing robust REST APIs and controllers.
What You’ll Learn in This Part
- Designing RESTful API endpoints with best practices
- Using DTOs (Data Transfer Objects) to separate API models from persistence entities
- Validating API requests using
javax.validation
annotations - Handling errors gracefully with
@ControllerAdvice
and custom exception handlers - Example of a CRUD controller for a simple
User
resource
Step 1: Define the User DTO
Instead of exposing our database entities directly (a bad practice), we create a UserDTO class that represents the API request and response model.
package com.example.demoapp.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class UserDTO {
private Long id;
@NotBlank(message = "Name is mandatory")
@Size(min = 2, max = 50)
private String name;
@Email(message = "Email should be valid")
@NotBlank(message = "Email is mandatory")
private String email;
// getters and setters omitted for brevity
}
Here, we use Jakarta Bean Validation annotations (@NotBlank
, @Email
, @Size
) to validate incoming data automatically.
Step 2: Create a Basic User Entity (for later persistence)
For now, just a simple entity to match:
package com.example.demoapp.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
private String name;
private String email;
// getters and setters
}
Step 3: Implement the UserController
Let’s create a REST controller for basic user operations.
package com.example.demoapp.controller;
import com.example.demoapp.dto.UserDTO;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController
@RequestMapping("/api/users")
public class UserController {
// Simple in-memory user store for demo
private Map<Long, UserDTO> userStore = new HashMap<>();
private Long idCounter = 1L;
// Create user
@PostMapping
public ResponseEntity<UserDTO> createUser(@Valid @RequestBody UserDTO userDto) {
userDto.setId(idCounter++);
userStore.put(userDto.getId(), userDto);
return new ResponseEntity<>(userDto, HttpStatus.CREATED);
}
// Get user by id
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
UserDTO user = userStore.get(id);
if (user == null) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
return ResponseEntity.ok(user);
}
// Get all users
@GetMapping
public List<UserDTO> getAllUsers() {
return new ArrayList<>(userStore.values());
}
// Update user
@PutMapping("/{id}")
public ResponseEntity<UserDTO> updateUser(@PathVariable Long id, @Valid @RequestBody UserDTO userDto) {
if (!userStore.containsKey(id)) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
userDto.setId(id);
userStore.put(id, userDto);
return ResponseEntity.ok(userDto);
}
// Delete user
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
if (!userStore.containsKey(id)) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
userStore.remove(id);
return ResponseEntity.noContent().build();
}
}
Step 4: Handle Validation Errors Gracefully
Add a global exception handler to catch validation errors and return meaningful JSON responses.
package com.example.demoapp.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach(error -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
}
Step 5: Testing Your API
Run your application and test the API endpoints:
- Create user:
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"name":"John Doe","email":"john.doe@example.com"}'
Response:
{
"id": 1,
"name": "John Doe",
"email": "john.doe@example.com"
}
- Get all users:
curl http://localhost:8080/api/users
Response:
[
{
"id": 1,
"name": "John Doe",
"email": "john.doe@example.com"
}
]
- Validation error example:
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"name":"","email":"not-an-email"}'
Response:
{
"name": "Name is mandatory",
"email": "Email should be valid"
}
Summary
In this part, you’ve learned how to:
- Design REST APIs using Spring MVC annotations
- Use DTOs with validation annotations for input safety
- Create CRUD endpoints
- Implement global exception handling for validation errors
What’s Next?
In Part 3, we’ll integrate persistence with JPA and Hibernate, replacing the in-memory store with a real database backend.