[Java Backend Zero to Hello] BÀI 5.6: SPRING BOOT REST API
📚 Bài viết thuộc series Java Backend Zero to Hello 📌 Phần: Phase 5: Spring Framework & Spring Boot | Bài 52/86
BÀI 5.6: SPRING BOOT REST API
Mục tiêu
- Xây dựng REST API với Spring Boot
- Sử dụng @RestController
- Xử lý JSON
- Status code và ResponseEntity
1. @RESTCONTROLLER
@RestController = @Controller + @ResponseBody
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public List<UserDTO> getAll() {
return userService.findAll();
}
@GetMapping("/{id}")
public UserDTO getById(@PathVariable Long id) {
return userService.findById(id)
.orElseThrow(() -> new NotFoundException("User not found"));
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public UserDTO create(@RequestBody @Valid CreateUserRequest request) {
return userService.create(request);
}
@PutMapping("/{id}")
public UserDTO update(@PathVariable Long id,
@RequestBody @Valid UpdateUserRequest request) {
return userService.update(id, request);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
userService.delete(id);
}
}
2. HTTP STATUS CODE
| Code | Ý nghĩa |
|---|---|
| 200 | OK - Thành công |
| 201 | Created - Tạo thành công |
| 204 | No Content - Xóa thành công |
| 400 | Bad Request - Request không hợp lệ |
| 401 | Unauthorized - Chưa xác thực |
| 403 | Forbidden - Không có quyền |
| 404 | Not Found - Không tìm thấy |
| 409 | Conflict - Xung đột |
| 500 | Internal Server Error - Lỗi server |
Sử dụng ResponseEntity
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getById(@PathVariable Long id) {
return userService.findById(id)
.map(user -> ResponseEntity.ok(user))
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<UserDTO> create(@RequestBody CreateUserRequest request) {
UserDTO created = userService.create(request);
URI location = URI.create("/api/users/" + created.getId());
return ResponseEntity.created(location).body(created);
}
3. JSON HANDLING
3.1 Jackson (mặc định)
Spring Boot dùng Jackson để convert Object ↔ JSON.
3.2 Cấu hình
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: Asia/Ho_Chi_Minh
serialization:
write-dates-as-timestamps: false
indent-output: true
deserialization:
fail-on-unknown-properties: false
3.3 Custom Serialization
@JsonProperty("full_name")
private String name;
@JsonIgnore
private String password;
@JsonFormat(pattern = "dd/MM/yyyy")
private LocalDate birthDate;
@JsonInclude(JsonInclude.Include.NON_NULL)
private String optionalField;
4. DTO PATTERN
Tách layer, không trả entity trực tiếp.
4.1 Request DTO
public record CreateUserRequest(
@NotBlank String name,
@Email String email,
@Min(0) @Max(150) Integer age
) {}
4.2 Response DTO
public record UserResponse(
Long id,
String name,
String email,
Integer age,
LocalDateTime createdAt
) {
public static UserResponse from(User user) {
return new UserResponse(
user.getId(),
user.getName(),
user.getEmail(),
user.getAge(),
user.getCreatedAt()
);
}
}
4.3 Sử dụng
@PostMapping
public UserResponse create(@RequestBody CreateUserRequest request) {
User user = userService.create(request);
return UserResponse.from(user);
}
5. VALIDATION
5.1 Dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
5.2 Annotation
public record CreateUserRequest(
@NotBlank(message = "Tên không được rỗng")
@Size(min = 2, max = 100)
String name,
@NotNull
@Email(message = "Email không hợp lệ")
String email,
@Min(value = 0, message = "Tuổi phải >= 0")
@Max(value = 150, message = "Tuổi phải <= 150")
Integer age,
@Pattern(regexp = "^\\d{10}$", message = "SĐT phải 10 chữ số")
String phone
) {}
5.3 Trong Controller
@PostMapping
public UserResponse create(@RequestBody @Valid CreateUserRequest request) {
// ...
}
5.4 Custom Validator
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = AdultValidator.class)
public @interface Adult {
String message() default "Phải >= 18 tuổi";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class AdultValidator implements ConstraintValidator<Adult, Integer> {
@Override
public boolean isValid(Integer age, ConstraintValidatorContext context) {
return age != null && age >= 18;
}
}
6. EXCEPTION HANDLING
6.1 Custom Exception
public class NotFoundException extends RuntimeException {
public NotFoundException(String message) {
super(message);
}
}
6.2 Error Response
public record ErrorResponse(
int status,
String message,
List<String> details,
LocalDateTime timestamp
) {}
6.3 Global Exception Handler
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(NotFoundException ex) {
ErrorResponse error = new ErrorResponse(
404, ex.getMessage(), null, LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(
MethodArgumentNotValidException ex) {
List<String> details = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
.collect(Collectors.toList());
ErrorResponse error = new ErrorResponse(
400, "Validation failed", details, LocalDateTime.now()
);
return ResponseEntity.badRequest().body(error);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneric(Exception ex) {
ErrorResponse error = new ErrorResponse(
500, "Internal server error", null, LocalDateTime.now()
);
return ResponseEntity.internalServerError().body(error);
}
}
7. CONTENT NEGOTIATION
@GetMapping(value = "/{id}", produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE})
public UserDTO getById(@PathVariable Long id) { ... }
8. CORS
@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")
.allowedHeaders("*")
.allowCredentials(true);
}
};
}
}
Hoặc trên controller:
@CrossOrigin(origins = "http://localhost:3000")
@RestController
public class UserController { ... }
9. VERSIONING API
// URI Versioning
@GetMapping("/v1/users")
@GetMapping("/v2/users")
// Header Versioning
@GetMapping(value = "/users", headers = "X-API-VERSION=1")
// Parameter Versioning
@GetMapping(value = "/users", params = "version=1")
// Media Type Versioning
@GetMapping(value = "/users", produces = "application/vnd.myapp.v1+json")
10. HATEOAS
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
@GetMapping("/{id}")
public EntityModel<UserResponse> getById(@PathVariable Long id) {
UserResponse user = userService.findById(id);
return EntityModel.of(user,
linkTo(methodOn(UserController.class).getById(id)).withSelfRel(),
linkTo(methodOn(UserController.class).getAll()).withRel("users")
);
}
11. BÀI TẬP THỰC HÀNH
Bài 1: User API
Tạo REST API cho User với đầy đủ CRUD + validation.
Bài 2: Error Handling
Implement global exception handler với ErrorResponse.
Bài 3: DTO Pattern
Tách entity và DTO, dùng MapStruct hoặc thủ công.
12. TÓM TẮT
| Khái niệm | Mô tả |
|---|---|
| @RestController | REST API controller |
| @RequestBody | Nhận JSON |
| ResponseEntity | Trả về với status code |
| DTO | Tách layer |
| @Valid | Validation |
| @RestControllerAdvice | Global exception handler |
| @JsonProperty | Custom JSON field |
| CORS | Cross-Origin |
Bài tiếp theo: 5.7 Validation
🧭 Điều Hướng Series
⬅️ Bài trước: BÀI 5.5: SPRING WEB MVC
📋 Lộ trình tổng quan: Xem Toàn Bộ Series
➡️ Bài tiếp theo: BÀI 5.7: VALIDATION
All rights reserved