Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:28:30 +08:00
commit 171acedaa4
220 changed files with 85967 additions and 0 deletions

View File

@@ -0,0 +1,315 @@
---
name: spring-boot-rest-api-standards
description: Implement REST API design standards and best practices for Spring Boot projects. Use when creating or reviewing REST endpoints, DTOs, error handling, pagination, security headers, HATEOAS and architecture patterns.
category: backend
tags: [spring-boot, rest-api, dto, validation, error-handling, pagination, hateoas, architecture, java]
version: 1.1.0
allowed-tools: Read, Write, Bash
---
# Spring Boot REST API Standards
This skill provides comprehensive guidance for building RESTful APIs in Spring Boot applications with consistent design patterns, proper error handling, validation, and architectural best practices based on REST principles and Spring Boot conventions.
## Overview
Spring Boot REST API standards establish consistent patterns for building production-ready REST APIs. These standards cover resource-based URL design, proper HTTP method usage, status code conventions, DTO patterns, validation, error handling, pagination, security headers, and architectural layering. Implement these patterns to ensure API consistency, maintainability, and adherence to REST principles.
## When to Use This Skill
Use this skill when:
- Creating new REST endpoints and API routes
- Designing request/response DTOs and API contracts
- Planning HTTP methods and status codes
- Implementing error handling and validation
- Setting up pagination, filtering, and sorting
- Designing security headers and CORS policies
- Implementing HATEOAS (Hypermedia As The Engine Of Application State)
- Reviewing REST API architecture and design patterns
- Building microservices with consistent API standards
- Documenting API endpoints with clear contracts
## Instructions
### To Build RESTful API Endpoints
Follow these steps to create well-designed REST API endpoints:
1. **Design Resource-Based URLs**
- Use plural nouns for resource names
- Follow REST conventions: GET /users, POST /users, PUT /users/{id}
- Avoid action-based URLs like /getUserList
2. **Implement Proper HTTP Methods**
- GET: Retrieve resources (safe, idempotent)
- POST: Create resources (not idempotent)
- PUT: Replace entire resources (idempotent)
- PATCH: Partial updates (not idempotent)
- DELETE: Remove resources (idempotent)
3. **Use Appropriate Status Codes**
- 200 OK: Successful GET/PUT/PATCH
- 201 Created: Successful POST with Location header
- 204 No Content: Successful DELETE
- 400 Bad Request: Invalid request data
- 404 Not Found: Resource doesn't exist
- 409 Conflict: Duplicate resource
- 500 Internal Server Error: Unexpected errors
4. **Create Request/Response DTOs**
- Separate API contracts from domain entities
- Use Java records or Lombok `@Data`/`@Value`
- Apply Jakarta validation annotations
- Keep DTOs immutable when possible
5. **Implement Validation**
- Use `@Valid` annotation on `@RequestBody` parameters
- Apply validation constraints (`@NotBlank`, `@Email`, `@Size`, etc.)
- Handle validation errors with `MethodArgumentNotValidException`
6. **Set Up Error Handling**
- Use `@RestControllerAdvice` for global exception handling
- Return standardized error responses with status, error, message, and timestamp
- Use `ResponseStatusException` for specific HTTP status codes
7. **Configure Pagination**
- Use Pageable for large datasets
- Include page, size, sort parameters
- Return metadata with total elements, totalPages, etc.
8. **Add Security Headers**
- Configure CORS policies
- Set content security policy
- Include X-Frame-Options, X-Content-Type-Options
## Examples
### Basic CRUD Controller
```java
@RestController
@RequestMapping("/v1/users")
@RequiredArgsConstructor
@Slf4j
public class UserController {
private final UserService userService;
@GetMapping
public ResponseEntity<Page<UserResponse>> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int pageSize) {
log.debug("Fetching users page {} size {}", page, pageSize);
Page<UserResponse> users = userService.getAll(page, pageSize);
return ResponseEntity.ok(users);
}
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getUserById(@PathVariable Long id) {
return ResponseEntity.ok(userService.getById(id));
}
@PostMapping
public ResponseEntity<UserResponse> createUser(@Valid @RequestBody CreateUserRequest request) {
UserResponse created = userService.create(request);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
@PutMapping("/{id}")
public ResponseEntity<UserResponse> updateUser(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
return ResponseEntity.ok(userService.update(id, request));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.noContent().build();
}
}
```
### Request/Response DTOs
```java
// Request DTO
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CreateUserRequest {
@NotBlank(message = "User name cannot be blank")
private String name;
@Email(message = "Valid email required")
private String email;
}
// Response DTO
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserResponse {
private Long id;
private String name;
private String email;
private LocalDateTime createdAt;
}
```
### Global Exception Handler
```java
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(
MethodArgumentNotValidException ex, WebRequest request) {
String errors = ex.getBindingResult().getFieldErrors().stream()
.map(f -> f.getField() + ": " + f.getDefaultMessage())
.collect(Collectors.joining(", "));
ErrorResponse errorResponse = new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
"Validation Error",
"Validation failed: " + errors,
request.getDescription(false).replaceFirst("uri=", "")
);
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<ErrorResponse> handleResponseStatusException(
ResponseStatusException ex, WebRequest request) {
ErrorResponse error = new ErrorResponse(
ex.getStatusCode().value(),
ex.getStatusCode().toString(),
ex.getReason(),
request.getDescription(false).replaceFirst("uri=", "")
);
return new ResponseEntity<>(error, ex.getStatusCode());
}
}
```
## Best Practices
### 1. Use Constructor Injection
```java
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
// Dependencies are explicit and testable
}
```
### 2. Prefer Immutable DTOs
```java
// Java records (JDK 16+)
public record UserResponse(Long id, String name, String email, LocalDateTime createdAt) {}
// Lombok @Value for immutability
@Value
public class UserResponse {
Long id;
String name;
String email;
LocalDateTime createdAt;
}
```
### 3. Validate Input Early
```java
@PostMapping
public ResponseEntity<UserResponse> createUser(@Valid @RequestBody CreateUserRequest request) {
// Validation happens automatically before method execution
return ResponseEntity.status(HttpStatus.CREATED).body(userService.create(request));
}
```
### 4. Use ResponseEntity Flexibly
```java
return ResponseEntity.status(HttpStatus.CREATED)
.header("Location", "/api/users/" + created.getId())
.header("X-Total-Count", String.valueOf(userService.count()))
.body(created);
```
### 5. Implement Proper Transaction Management
```java
@Service
@Transactional
public class UserService {
@Transactional(readOnly = true)
public Optional<User> findById(Long id) {
return userRepository.findById(id);
}
@Transactional
public User create(User user) {
return userRepository.save(user);
}
}
```
### 6. Add Meaningful Logging
```java
@Slf4j
@Service
public class UserService {
public User create(User user) {
log.info("Creating user with email: {}", user.getEmail());
return userRepository.save(user);
}
}
```
### 7. Document APIs with Javadoc
```java
/**
* Retrieves a user by id.
*
* @param id the user id
* @return ResponseEntity containing a UserResponse
* @throws ResponseStatusException with 404 if user not found
*/
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getUserById(@PathVariable Long id)
```
## Constraints
### 1. Never Expose Entities Directly
Use DTOs to separate API contracts from domain models. This prevents accidental exposure of internal data structures and allows API evolution without database schema changes.
### 2. Follow REST Conventions Strictly
- Use nouns for resource names, not verbs
- Use correct HTTP methods for operations
- Use plural resource names (/users, not /user)
- Return appropriate HTTP status codes for each operation
### 3. Handle All Exceptions Globally
Use @RestControllerAdvice to catch all exceptions consistently. Don't let raw exceptions bubble up to clients.
### 4. Always Paginate Large Result Sets
For GET endpoints that might return many results, implement pagination to prevent performance issues and DDoS vulnerabilities.
### 5. Validate All Input Data
Never trust client input. Use Jakarta validation annotations on all request DTOs to validate data at the controller boundary.
### 6. Use Constructor Injection Exclusively
Avoid field injection (`@Autowired`) for better testability and explicit dependency declaration.
### 7. Keep Controllers Thin
Controllers should only handle HTTP request/response adaptation. Delegate business logic to service layers.
## References
- See `references/` directory for comprehensive reference material including HTTP status codes, Spring annotations, and detailed examples
- Refer to `agents/spring-boot-code-review-specialist.md` for code review guidelines
- Review `spring-boot-dependency-injection/SKILL.md` for dependency injection patterns
- Check `junit-test-patterns/SKILL.md` for testing REST APIs

View File

@@ -0,0 +1,761 @@
# Architecture Patterns for REST APIs
## Layered Architecture
### Feature-Based Structure
```
feature-name/
├── domain/
│ ├── model/ # Domain entities (Spring-free)
│ │ └── User.java
│ ├── repository/ # Domain ports (interfaces)
│ │ └── UserRepository.java
│ └── service/ # Domain services
│ └── UserService.java
├── application/
│ ├── service/ # Use cases (@Service beans)
│ │ └── UserApplicationService.java
│ └── dto/ # Immutable DTOs/records
│ ├── UserRequest.java
│ └── UserResponse.java
├── presentation/
│ └── rest/ # Controllers and mappers
│ ├── UserController.java
│ ├── UserMapper.java
│ └── UserExceptionHandler.java
└── infrastructure/
└── persistence/ # JPA adapters
└── JpaUserRepository.java
```
### Domain Layer (Clean Architecture)
#### Domain Entity
```java
package com.example.domain.model;
import java.time.LocalDateTime;
import java.util.Objects;
public class User {
private final UserId id;
private final String name;
private final Email email;
private final LocalDateTime createdAt;
private final LocalDateTime updatedAt;
private User(UserId id, String name, Email email) {
this.id = Objects.requireNonNull(id);
this.name = Objects.requireNonNull(name);
this.email = Objects.requireNonNull(email);
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
public static User create(UserId id, String name, Email email) {
return new User(id, name, email);
}
public User updateName(String newName) {
return new User(this.id, newName, this.email);
}
public User updateEmail(Email newEmail) {
return new User(this.id, this.name, newEmail);
}
// Getters
public UserId getId() { return id; }
public String getName() { return name; }
public Email getEmail() { return email; }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
}
// Value objects
public class UserId {
private final Long value;
public UserId(Long value) {
this.value = Objects.requireNonNull(value);
if (value <= 0) {
throw new IllegalArgumentException("User ID must be positive");
}
}
public Long getValue() { return value; }
}
public class Email {
private final String value;
public Email(String value) {
this.value = Objects.requireNonNull(value);
if (!isValid(value)) {
throw new IllegalArgumentException("Invalid email format");
}
}
private boolean isValid(String email) {
return email.contains("@") && email.length() > 5;
}
public String getValue() { return value; }
}
```
#### Domain Repository Port
```java
package com.example.domain.repository;
import com.example.domain.model.User;
import com.example.domain.model.UserId;
import java.util.Optional;
public interface UserRepository {
Optional<User> findById(UserId id);
void save(User user);
void delete(UserId id);
boolean existsByEmail(Email email);
}
```
#### Domain Service
```java
package com.example.domain.service;
import com.example.domain.model.User;
import com.example.domain.model.Email;
import com.example.domain.repository.UserRepository;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class UserDomainService {
private final UserRepository userRepository;
public User registerUser(String name, String email) {
Email emailObj = new Email(email);
if (userRepository.existsByEmail(emailObj)) {
throw new BusinessException("Email already exists");
}
User user = User.create(UserId.generate(), name, emailObj);
userRepository.save(user);
return user;
}
}
```
### Application Layer (Use Cases)
#### Application Service
```java
package com.example.application.service;
import com.example.application.dto.UserRequest;
import com.example.application.dto.UserResponse;
import com.example.domain.model.User;
import com.example.domain.repository.UserRepository;
import com.example.application.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@Service
@RequiredArgsConstructor
@Slf4j
public class UserApplicationService {
private final UserRepository userRepository;
private final UserMapper userMapper;
@Transactional
public UserResponse createUser(UserRequest request) {
log.info("Creating user: {}", request.getName());
User user = userMapper.toDomain(request);
User saved = userRepository.save(user);
return userMapper.toResponse(saved);
}
@Transactional(readOnly = true)
public Page<UserResponse> findAllUsers(Pageable pageable) {
return userRepository.findAll(pageable)
.map(userMapper::toResponse);
}
@Transactional(readOnly = true)
public UserResponse findUserById(Long id) {
return userRepository.findById(new UserId(id))
.map(userMapper::toResponse)
.orElseThrow(() -> new EntityNotFoundException("User not found"));
}
@Transactional
public UserResponse updateUser(Long id, UserRequest request) {
User user = userRepository.findById(new UserId(id))
.orElseThrow(() -> new EntityNotFoundException("User not found"));
User updated = user.updateName(request.getName());
User saved = userRepository.save(updated);
return userMapper.toResponse(saved);
}
@Transactional
public void deleteUser(Long id) {
userRepository.delete(new UserId(id));
}
}
```
#### DTOs
```java
package com.example.application.dto;
import lombok.Value;
import jakarta.validation.constraints.*;
@Value
public class UserRequest {
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100, message = "Name must be 2-100 characters")
private String name;
@NotBlank(message = "Email is required")
@Email(message = "Valid email required")
private String email;
}
@Value
public class UserResponse {
Long id;
String name;
String email;
LocalDateTime createdAt;
LocalDateTime updatedAt;
}
```
### Presentation Layer (REST API)
#### Controller
```java
package com.example.presentation.rest;
import com.example.application.dto.UserRequest;
import com.example.application.dto.UserResponse;
import com.example.application.service.UserApplicationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
@Slf4j
public class UserController {
private final UserApplicationService userApplicationService;
@PostMapping
public ResponseEntity<UserResponse> createUser(@Valid @RequestBody UserRequest request) {
UserResponse created = userApplicationService.createUser(request);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
@GetMapping
public ResponseEntity<Page<UserResponse>> getAllUsers(Pageable pageable) {
Page<UserResponse> users = userApplicationService.findAllUsers(pageable);
return ResponseEntity.ok(users);
}
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getUserById(@PathVariable Long id) {
UserResponse user = userApplicationService.findUserById(id);
return ResponseEntity.ok(user);
}
@PutMapping("/{id}")
public ResponseEntity<UserResponse> updateUser(
@PathVariable Long id,
@Valid @RequestBody UserRequest request) {
UserResponse updated = userApplicationService.updateUser(id, request);
return ResponseEntity.ok(updated);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userApplicationService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
```
#### Mapper
```java
package com.example.application.mapper;
import com.example.application.dto.UserRequest;
import com.example.application.dto.UserResponse;
import com.example.domain.model.User;
import com.example.domain.model.Email;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy;
@Mapper(componentModel = "spring")
public interface UserMapper {
@Mapping(target = "id", source = "id.value")
UserResponse toResponse(User user);
User toDomain(UserRequest request);
default Email toEmail(String email) {
return new Email(email);
}
default String toString(Email email) {
return email.getValue();
}
}
```
### Infrastructure Layer (Adapters)
#### JPA Repository Adapter
```java
package com.example.infrastructure.persistence;
import com.example.domain.model.User;
import com.example.domain.model.UserId;
import com.example.domain.repository.UserRepository;
import com.example.infrastructure.entity.UserEntity;
import com.example.infrastructure.mapper.UserEntityMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
@Repository
@RequiredArgsConstructor
public class JpaUserRepository implements UserRepository {
private final SpringDataUserRepository springDataRepository;
private final UserEntityMapper entityMapper;
@Override
public Optional<User> findById(UserId id) {
return springDataRepository.findById(id.getValue())
.map(entityMapper::toDomain);
}
@Override
public void save(User user) {
UserEntity entity = entityMapper.toEntity(user);
springDataRepository.save(entity);
}
@Override
public void delete(UserId id) {
springDataRepository.deleteById(id.getValue());
}
@Override
public boolean existsByEmail(Email email) {
return springDataRepository.existsByEmail(email.getValue());
}
}
```
#### Spring Data Repository
```java
package com.example.infrastructure.persistence;
import com.example.infrastructure.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
@Repository
public interface SpringDataUserRepository extends JpaRepository<UserEntity, Long> {
boolean existsByEmail(String email);
@Query("SELECT u FROM UserEntity u WHERE u.email = :email")
Optional<UserEntity> findByEmail(@Param("email") String email);
}
```
## CQRS Pattern (Command Query Responsibility Segregation)
### Commands
```java
package com.example.application.command;
import lombok.Value;
import jakarta.validation.constraints.*;
@Value
public class CreateUserCommand {
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100)
private String name;
@NotBlank(message = "Email is required")
@Email
private String email;
}
@Value
public class UpdateUserCommand {
@NotNull(message = "User ID is required")
private Long userId;
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100)
private String name;
}
```
### Command Handlers
```java
package com.example.application.command.handler;
import com.example.application.command.CreateUserCommand;
import com.example.application.command.UpdateUserCommand;
import com.example.domain.model.User;
import com.example.domain.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Slf4j
public class UserCommandHandler {
private final UserRepository userRepository;
@Transactional
public User handle(CreateUserCommand command) {
User user = User.create(UserId.generate(), command.getName(), new Email(command.getEmail()));
return userRepository.save(user);
}
@Transactional
public User handle(UpdateUserCommand command) {
User user = userRepository.findById(new UserId(command.getUserId()))
.orElseThrow(() -> new EntityNotFoundException("User not found"));
User updated = user.updateName(command.getName());
return userRepository.save(updated);
}
}
```
### Queries
```java
package com.example.application.query;
import lombok.Value;
import java.util.List;
@Value
public class FindAllUsersQuery {
int page;
int size;
String sortBy;
String sortDirection;
}
@Value
public class FindUserByIdQuery {
Long userId;
}
```
### Query Handlers
```java
package com.example.application.query.handler;
import com.example.application.query.FindAllUsersQuery;
import com.example.application.query.FindUserByIdQuery;
import com.example.application.dto.UserResponse;
import com.example.domain.model.User;
import com.example.domain.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Slf4j
public class UserQueryHandler {
private final UserRepository userRepository;
public Page<UserResponse> handle(FindAllUsersQuery query) {
Pageable pageable = PageRequest.of(
query.getPage(),
query.getSize(),
Sort.by(Sort.Direction.fromString(query.getSortDirection()), query.getSortBy())
);
return userRepository.findAll(pageable)
.map(this::toResponse);
}
public UserResponse handle(FindUserByIdQuery query) {
return userRepository.findById(new UserId(query.getUserId()))
.map(this::toResponse)
.orElseThrow(() -> new EntityNotFoundException("User not found"));
}
private UserResponse toResponse(User user) {
return new UserResponse(
user.getId().getValue(),
user.getName(),
user.getEmail().getValue(),
user.getCreatedAt(),
user.getUpdatedAt()
);
}
}
```
## Event-Driven Architecture
### Domain Events
```java
package com.example.domain.event;
import lombok.Value;
import java.time.LocalDateTime;
@Value
public class UserCreatedEvent {
String userId;
String email;
LocalDateTime timestamp;
}
@Value
public class UserUpdatedEvent {
String userId;
String oldName;
String newName;
LocalDateTime timestamp;
}
```
### Event Publisher
```java
package com.example.domain.service;
import com.example.domain.event.UserCreatedEvent;
import com.example.domain.event.UserUpdatedEvent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Slf4j
public class UserEventPublisher {
private final ApplicationEventPublisher eventPublisher;
public void publishUserCreated(String userId, String email) {
UserCreatedEvent event = new UserCreatedEvent(userId, email, LocalDateTime.now());
eventPublisher.publishEvent(event);
log.info("Published UserCreatedEvent for user: {}", userId);
}
public void publishUserUpdated(String userId, String oldName, String newName) {
UserUpdatedEvent event = new UserUpdatedEvent(userId, oldName, newName, LocalDateTime.now());
eventPublisher.publishEvent(event);
log.info("Published UserUpdatedEvent for user: {}", userId);
}
}
```
### Event Listeners
```java
package com.example.application.event.listener;
import com.example.domain.event.UserCreatedEvent;
import com.example.domain.event.UserUpdatedEvent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
@Slf4j
public class UserEventListener {
@EventListener
public void handleUserCreated(UserCreatedEvent event) {
log.info("User created: {} with email: {}", event.getUserId(), event.getEmail());
// Send welcome email, update analytics, etc.
}
@EventListener
public void handleUserUpdated(UserUpdatedEvent event) {
log.info("User {} updated: {} -> {}", event.getUserId(), event.getOldName(), event.getNewName());
// Update search index, send notification, etc.
}
}
```
## Microservices Architecture
### API Gateway Pattern
```java
package com.example.gateway;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("user-service", r -> r.path("/api/users/**")
.filters(f -> f.stripPrefix(1))
.uri("lb://user-service"))
.route("order-service", r -> r.path("/api/orders/**")
.filters(f -> f.stripPrefix(1))
.uri("lb://order-service"))
.build();
}
}
```
### Service Communication
```java
package com.example.application.client;
import com.example.application.dto.UserResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(name = "user-service", url = "${user.service.url}")
public interface UserServiceClient {
@GetMapping("/api/users/{id}")
UserResponse getUserById(@PathVariable Long id);
@GetMapping("/api/users")
List<UserResponse> getAllUsers();
}
```
## Testing Strategy
### Unit Tests
```java
package com.example.application.service;
import com.example.application.dto.UserRequest;
import com.example.domain.model.User;
import com.example.domain.repository.UserRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class UserApplicationServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserApplicationService userApplicationService;
@Test
void createUserShouldCreateUser() {
// Arrange
UserRequest request = new UserRequest("John Doe", "john@example.com");
User user = User.create(UserId.generate(), "John Doe", new Email("john@example.com"));
when(userRepository.save(any(User.class))).thenReturn(user);
// Act
UserResponse result = userApplicationService.createUser(request);
// Assert
assertNotNull(result);
assertEquals("John Doe", result.getName());
verify(userRepository).save(any(User.class));
}
}
```
### Integration Tests
```java
package com.example.presentation.rest;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
class UserControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
void createUserShouldReturnCreatedStatus() throws Exception {
// Arrange
String requestBody = """
{
"name": "John Doe",
"email": "john@example.com"
}
""";
// Act & Assert
mockMvc.perform(post("/api/users")
.contentType("application/json")
.content(requestBody))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.name").value("John Doe"))
.andExpect(jsonPath("$.email").value("john@example.com"));
}
}
```

View File

@@ -0,0 +1,597 @@
# Spring Boot REST API Examples
## Complete CRUD REST API with Validation
### Entity with Validation
```java
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100, message = "Name must be 2-100 characters")
private String name;
@NotBlank(message = "Email is required")
@Email(message = "Valid email required")
@Column(unique = true)
private String email;
@Min(value = 18, message = "Must be at least 18")
@Max(value = 120, message = "Invalid age")
private Integer age;
@Size(min = 8, max = 100, message = "Password must be 8-100 characters")
private String password;
@Column(name = "is_active")
private Boolean active = true;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}
```
### Service with Transaction Management
```java
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
@Transactional(readOnly = true)
public Page<UserResponse> findAll(Pageable pageable) {
log.debug("Fetching users page {} size {}", pageable.getPageNumber(), pageable.getPageSize());
return userRepository.findAll(pageable)
.map(this::toResponse);
}
@Transactional(readOnly = true)
public UserResponse findById(Long id) {
log.debug("Looking for user with id {}", id);
return userRepository.findById(id)
.map(this::toResponse)
.orElseThrow(() -> new EntityNotFoundException("User not found"));
}
@Transactional
public UserResponse create(CreateUserRequest request) {
log.info("Creating user with email: {}", request.getEmail());
if (userRepository.existsByEmail(request.getEmail())) {
throw new BusinessException("Email already exists");
}
User user = new User();
user.setName(request.getName());
user.setEmail(request.getEmail());
user.setAge(request.getAge());
user.setPassword(passwordEncoder.encode(request.getPassword()));
User saved = userRepository.save(user);
emailService.sendWelcomeEmail(saved.getEmail(), saved.getName());
return toResponse(saved);
}
@Transactional
public UserResponse update(Long id, UpdateUserRequest request) {
User user = userRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("User not found"));
if (request.getName() != null) {
user.setName(request.getName());
}
if (request.getEmail() != null) {
if (!user.getEmail().equals(request.getEmail()) &&
userRepository.existsByEmail(request.getEmail())) {
throw new BusinessException("Email already exists");
}
user.setEmail(request.getEmail());
}
if (request.getAge() != null) {
user.setAge(request.getAge());
}
User updated = userRepository.save(user);
return toResponse(updated);
}
@Transactional
public void delete(Long id) {
if (!userRepository.existsById(id)) {
throw new EntityNotFoundException("User not found");
}
User user = userRepository.findById(id).orElseThrow();
emailService.sendDeletionEmail(user.getEmail(), user.getName());
userRepository.deleteById(id);
}
private UserResponse toResponse(User user) {
return new UserResponse(
user.getId(),
user.getName(),
user.getEmail(),
user.getAge(),
user.getActive(),
user.getCreatedAt(),
user.getUpdatedAt()
);
}
}
```
### Controller with Proper HTTP Methods
```java
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
@Slf4j
public class UserController {
private final UserService userService;
@GetMapping
public ResponseEntity<Page<UserResponse>> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "createdAt") String sortBy,
@RequestParam(defaultValue = "DESC") String sortDirection) {
Sort sort = Sort.by(Sort.Direction.fromString(sortDirection), sortBy);
Pageable pageable = PageRequest.of(page, size, sort);
Page<UserResponse> users = userService.findAll(pageable);
HttpHeaders headers = new HttpHeaders();
headers.add("X-Total-Count", String.valueOf(users.getTotalElements()));
headers.add("X-Total-Pages", String.valueOf(users.getTotalPages()));
return ResponseEntity.ok()
.headers(headers)
.body(users);
}
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getUserById(@PathVariable Long id) {
return ResponseEntity.ok(userService.findById(id));
}
@PostMapping
public ResponseEntity<UserResponse> createUser(@Valid @RequestBody CreateUserRequest request) {
UserResponse created = userService.create(request);
return ResponseEntity.status(HttpStatus.CREATED)
.header("Location", "/api/users/" + created.getId())
.body(created);
}
@PutMapping("/{id}")
public ResponseEntity<UserResponse> updateUser(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
UserResponse updated = userService.update(id, request);
return ResponseEntity.ok(updated);
}
@PatchMapping("/{id}")
public ResponseEntity<UserResponse> patchUser(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
UserResponse updated = userService.update(id, request);
return ResponseEntity.ok(updated);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.noContent().build();
}
}
```
## API Versioning Examples
### URL Versioning
```java
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 {
// Version 1 endpoints
}
@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 {
// Version 2 endpoints with different response format
}
```
### Header Versioning
```java
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping
public ResponseEntity<UserResponse> getUsers(
@RequestHeader(value = "Accept-Version", defaultValue = "1.0") String version) {
if (version.equals("2.0")) {
return ResponseEntity.ok(v2UserResponse);
}
return ResponseEntity.ok(v1UserResponse);
}
}
```
### Media Type Versioning
```java
@GetMapping(produces = {
"application/vnd.company.v1+json",
"application/vnd.company.v2+json"
})
public ResponseEntity<UserResponse> getUsers(
@RequestHeader("Accept") String accept) {
if (accept.contains("v2")) {
return ResponseEntity.ok(v2UserResponse);
}
return ResponseEntity.ok(v1UserResponse);
}
```
## HATEOAS Implementation
### Response with Links
```java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserResponseWithLinks {
private Long id;
private String name;
private String email;
private Map<String, String> _links;
// Lombok generates constructors/getters/setters
}
@GetMapping("/{id}")
public ResponseEntity<UserResponseWithLinks> getUserWithLinks(@PathVariable Long id) {
UserResponse user = userService.findById(id);
Map<String, String> links = Map.of(
"self", "/api/users/" + id,
"all", "/api/users",
"update", "/api/users/" + id,
"delete", "/api/users/" + id
);
UserResponseWithLinks response = new UserResponseWithLinks(
user.getId(), user.getName(), user.getEmail(), links);
return ResponseEntity.ok(response);
}
```
### Advanced HATEOAS with Spring HATEOAS
```java
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
private final EntityLinks entityLinks;
@GetMapping
public ResponseEntity<CollectionModel<UserResponse>> getAllUsers() {
List<UserResponse> users = userService.findAll().stream()
.map(this::toResponse)
.collect(Collectors.toList());
CollectionModel<UserResponse> resource = CollectionModel.of(users);
resource.add(entityLinks.linkToCollectionResource(UserController.class).withSelfRel());
resource.add(entityLinks.linkToCollectionResource(UserController.class).withRel("users"));
return ResponseEntity.ok(resource);
}
@GetMapping("/{id}")
public ResponseEntity<EntityModel<UserResponse>> getUserById(@PathVariable Long id) {
UserResponse user = userService.findById(id);
EntityModel<UserResponse> resource = EntityModel.of(user);
resource.add(entityLinks.linkToItemResource(UserController.class, id).withSelfRel());
resource.add(entityLinks.linkToCollectionResource(UserController.class).withRel("users"));
resource.add(linkTo(methodOn(UserController.class).getUserOrders(id)).withRel("orders"));
return ResponseEntity.ok(resource);
}
private UserResponse toResponse(User user) {
return new UserResponse(
user.getId(),
user.getName(),
user.getEmail(),
user.getActive(),
user.getCreatedAt()
);
}
}
```
## Async Processing
### Asynchronous Controller
```java
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class AsyncUserController {
private final AsyncUserService asyncUserService;
@GetMapping("/{id}")
public CompletableFuture<ResponseEntity<UserResponse>> getUserById(@PathVariable Long id) {
return asyncUserService.getUserById(id)
.thenApply(ResponseEntity::ok)
.exceptionally(ex -> ResponseEntity.notFound().build());
}
@PostMapping
public CompletableFuture<ResponseEntity<UserResponse>> createUser(
@Valid @RequestBody CreateUserRequest request) {
return asyncUserService.createUser(request)
.thenApply(created ->
ResponseEntity.status(HttpStatus.CREATED)
.header("Location", "/api/users/" + created.getId())
.body(created))
.exceptionally(ex -> {
if (ex.getCause() instanceof BusinessException) {
return ResponseEntity.badRequest().build();
}
return ResponseEntity.internalServerError().build();
});
}
}
```
### Async Service Implementation
```java
@Service
@RequiredArgsConstructor
public class AsyncUserService {
private final UserService userService;
private final ExecutorService executor;
@Async
public CompletableFuture<UserResponse> getUserById(Long id) {
return CompletableFuture.supplyAsync(() -> userService.findById(id), executor);
}
@Async
public CompletableFuture<UserResponse> createUser(CreateUserRequest request) {
return CompletableFuture.supplyAsync(() -> userService.create(request), executor);
}
}
```
## File Upload and Download
### File Upload Controller
```java
@RestController
@RequestMapping("/api/files")
@RequiredArgsConstructor
public class FileController {
private final FileStorageService fileStorageService;
@PostMapping("/upload")
public ResponseEntity<FileUploadResponse> uploadFile(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
throw new BusinessException("File is empty");
}
String fileName = fileStorageService.storeFile(file);
String fileDownloadUri = ServletUriComponentsBuilder.fromCurrentContextPath()
.path("/api/files/download/")
.path(fileName)
.toUriString();
FileUploadResponse response = new FileUploadResponse(
fileName, fileDownloadUri, file.getContentType(), file.getSize());
return ResponseEntity.ok(response);
}
@GetMapping("/download/{fileName:.+}")
public ResponseEntity<Resource> downloadFile(@PathVariable String fileName) {
Resource resource = fileStorageService.loadFileAsResource(fileName);
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + resource.getFilename() + "\"")
.body(resource);
}
}
```
### File Storage Service
```java
@Service
public class FileStorageService {
private final Path fileStorageLocation;
@Autowired
public FileStorageService(FileStorageProperties fileStorageProperties) {
this.fileStorageLocation = Paths.get(fileStorageProperties.getUploadDir())
.toAbsolutePath().normalize();
try {
Files.createDirectories(this.fileStorageLocation);
} catch (Exception ex) {
throw new FileStorageException("Could not create the directory where the uploaded files will be stored.", ex);
}
}
public String storeFile(MultipartFile file) {
String fileName = StringUtils.cleanPath(Objects.requireNonNull(file.getOriginalFilename()));
try {
if (fileName.contains("..")) {
throw new FileStorageException("Sorry! Filename contains invalid path sequence " + fileName);
}
Path targetLocation = this.fileStorageLocation.resolve(fileName);
Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING);
return fileName;
} catch (IOException ex) {
throw new FileStorageException("Could not store file " + fileName + ". Please try again!", ex);
}
}
public Resource loadFileAsResource(String fileName) {
try {
Path filePath = this.fileStorageLocation.resolve(fileName).normalize();
Resource resource = new UrlResource(filePath);
if (resource.exists() && resource.isReadable()) {
return resource;
} else {
throw new FileNotFoundException("File not found " + fileName);
}
} catch (Exception ex) {
throw new FileNotFoundException("File not found " + fileName, ex);
}
}
}
```
## WebSocket Integration
### WebSocket Configuration
```java
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS();
}
}
```
### WebSocket Controller
```java
@Controller
@RequiredArgsConstructor
public class WebSocketController {
private final SimpMessagingTemplate messagingTemplate;
@MessageMapping("/chat.sendMessage")
@SendTo("/topic/public")
public ChatMessage sendMessage(@Payload ChatMessage chatMessage) {
return chatMessage;
}
@MessageMapping("/chat.addUser")
@SendTo("/topic/public")
public ChatMessage addUser(@Payload ChatMessage chatMessage,
SimpMessageHeaderAccessor headerAccessor) {
headerAccessor.getSessionAttributes().put("username", chatMessage.getSender());
return chatMessage;
}
@Scheduled(fixedRate = 5000)
public void sendPeriodicUpdates() {
messagingTemplate.convertAndSend("/topic/updates",
new UpdateMessage("System update", LocalDateTime.now()));
}
}
```
### Frontend Integration Example
```javascript
// JavaScript WebSocket client
class WebSocketClient {
constructor(url) {
this.url = url;
this.stompClient = null;
this.connected = false;
}
connect() {
const socket = new SockJS(this.url);
this.stompClient = Stomp.over(socket);
this.stompClient.connect({}, (frame) => {
this.connected = true;
console.log('Connected: ' + frame);
// Subscribe to topics
this.stompClient.subscribe('/topic/public', (message) => {
this.onMessage(message);
});
this.stompClient.subscribe('/topic/updates', (update) => {
this.onUpdate(update);
});
}, (error) => {
this.connected = false;
console.error('Error: ' + error);
});
}
sendMessage(message) {
if (this.connected) {
this.stompClient.send("/app/chat.sendMessage", {}, JSON.stringify(message));
}
}
onMessage(message) {
const chatMessage = JSON.parse(message.body);
console.log('Received message:', chatMessage);
// Display message in UI
}
onUpdate(update) {
const updateMessage = JSON.parse(update.body);
console.log('Received update:', updateMessage);
// Update UI with system messages
}
}
```

View File

@@ -0,0 +1,88 @@
# HTTP Methods and Status Codes Reference
## HTTP Methods
| Method | Idempotent | Safe | Purpose | Typical Status |
|--------|-----------|------|---------|----------------|
| GET | Yes | Yes | Retrieve resource | 200, 304, 404 |
| POST | No | No | Create resource | 201, 400, 409 |
| PUT | Yes | No | Replace resource | 200, 204, 404 |
| PATCH | No | No | Partial update | 200, 204, 400 |
| DELETE | Yes | No | Remove resource | 204, 404 |
| HEAD | Yes | Yes | Like GET, no body | 200, 304, 404 |
| OPTIONS | Yes | Yes | Describe communication options | 200 |
### Idempotent Operations
An operation is idempotent if making the same request multiple times produces the same result as making it once.
### Safe Operations
A safe operation doesn't change the state of the server. Safe operations are always idempotent.
## HTTP Status Codes
### 2xx Success
- `200 OK`: Successful GET/PUT/PATCH
- `201 Created`: Successful POST (include Location header)
- `202 Accepted`: Async processing accepted
- `204 No Content`: Successful DELETE or POST with no content
- `206 Partial Content`: Range request successful
### 3xx Redirection
- `301 Moved Permanently`: Resource permanently moved
- `304 Not Modified`: Cache valid, use local copy
- `307 Temporary Redirect`: Temporary redirect
### 4xx Client Errors
- `400 Bad Request`: Invalid format or parameters
- `401 Unauthorized`: Authentication required
- `403 Forbidden`: Authenticated but not authorized
- `404 Not Found`: Resource doesn't exist
- `409 Conflict`: Constraint violation or conflict
- `422 Unprocessable Entity`: Validation failed (semantic error)
- `429 Too Many Requests`: Rate limit exceeded
### 5xx Server Errors
- `500 Internal Server Error`: Unexpected server error
- `502 Bad Gateway`: External service unavailable
- `503 Service Unavailable`: Server temporarily down
- `504 Gateway Timeout`: External service timeout
## Common REST API Patterns
### Resource URLs
```
GET /users # List all users
GET /users/123 # Get specific user
POST /users # Create user
PUT /users/123 # Update user
DELETE /users/123 # Delete user
GET /users/123/orders # Get user's orders
```
### Query Parameters
```
GET /users?page=0&size=20&sort=createdAt,desc
- page: Page number (0-based)
- size: Number of items per page
- sort: Sorting format (field,direction)
```
### Response Headers
```
Location: /api/users/123 # For 201 Created responses
X-Total-Count: 45 # Total items count
Cache-Control: no-cache # Cache control
Content-Type: application/json # Response format
```
## Error Response Format
```json
{
"status": 400,
"error": "Bad Request",
"message": "Validation failed: name: Name cannot be blank, email: Valid email required",
"path": "/api/users",
"timestamp": "2024-01-15T10:30:00Z"
}
```

View File

@@ -0,0 +1,387 @@
# Pagination and Filtering Reference
## Pagination with Spring Data
### Basic Pagination
```java
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping
public ResponseEntity<Page<UserResponse>> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
Pageable pageable = PageRequest.of(page, size);
Page<UserResponse> users = userService.findAll(pageable);
return ResponseEntity.ok(users);
}
}
```
### Pagination with Sorting
```java
@GetMapping("/users")
public ResponseEntity<Page<UserResponse>> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "createdAt") String sortBy,
@RequestParam(defaultValue = "DESC") String sortDirection) {
Sort sort = Sort.by(Sort.Direction.fromString(sortDirection), sortBy);
Pageable pageable = PageRequest.of(page, size, sort);
Page<UserResponse> users = userService.findAll(pageable);
return ResponseEntity.ok(users);
}
```
### Multi-field Sorting
```java
@GetMapping("/users")
public ResponseEntity<Page<UserResponse>> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "name") String sortBy,
@RequestParam(defaultValue = "name,createdAt") String sortFields) {
List<Sort.Order> orders = Arrays.stream(sortFields.split(","))
.map(field -> {
String direction = field.startsWith("-") ? "DESC" : "ASC";
String property = field.startsWith("-") ? field.substring(1) : field;
return new Sort.Order(Sort.Direction.fromString(direction), property);
})
.collect(Collectors.toList());
Pageable pageable = PageRequest.of(page, size, Sort.by(orders));
Page<UserResponse> users = userService.findAll(pageable);
return ResponseEntity.ok(users);
}
```
## Response Format
### Standard Page Response
```json
{
"content": [
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"createdAt": "2024-01-15T10:30:00Z"
}
],
"pageable": {
"sort": {
"empty": false,
"sorted": true,
"unsorted": false
},
"offset": 0,
"pageNumber": 0,
"pageSize": 10,
"paged": true,
"unpaged": false
},
"last": false,
"totalPages": 5,
"totalElements": 45,
"size": 10,
"number": 0,
"sort": {
"empty": false,
"sorted": true,
"unsorted": false
},
"first": true,
"numberOfElements": 10,
"empty": false
}
```
### Custom Page Response Wrapper
```java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageResponse<T> {
private List<T> content;
private PageMetadata metadata;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageMetadata {
private int pageNumber;
private int pageSize;
private long totalElements;
private int totalPages;
private boolean first;
private boolean last;
}
// Controller
@GetMapping("/users")
public ResponseEntity<PageResponse<UserResponse>> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
Page<User> pageResult = userService.findAll(pageable);
PageMetadata metadata = new PageMetadata(
page,
size,
pageResult.getTotalElements(),
pageResult.getTotalPages(),
pageResult.isFirst(),
pageResult.isLast()
);
PageResponse<UserResponse> response = new PageResponse<>(
pageResult.stream()
.map(this::toResponse)
.collect(Collectors.toList()),
metadata
);
return ResponseEntity.ok(response);
}
```
## Filtering
### Query Parameter Filtering
```java
@GetMapping("/users")
public ResponseEntity<Page<UserResponse>> getUsers(
@RequestParam(required = false) String name,
@RequestParam(required = false) String email,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
Specification<User> spec = Specification.where(null);
if (name != null && !name.isEmpty()) {
spec = spec.and((root, query, cb) ->
cb.like(cb.lower(root.get("name")), "%" + name.toLowerCase() + "%"));
}
if (email != null && !email.isEmpty()) {
spec = spec.and((root, query, cb) ->
cb.like(cb.lower(root.get("email")), "%" + email.toLowerCase() + "%"));
}
Pageable pageable = PageRequest.of(page, size);
Page<User> pageResult = userService.findAll(spec, pageable);
return ResponseEntity.ok(pageResult.map(this::toResponse));
}
```
### Dynamic Specification Builder
```java
public class UserSpecifications {
public static Specification<User> hasName(String name) {
return (root, query, cb) ->
name == null ? null :
cb.like(cb.lower(root.get("name")), "%" + name.toLowerCase() + "%");
}
public static Specification<User> hasEmail(String email) {
return (root, query, cb) ->
email == null ? null :
cb.like(cb.lower(root.get("email")), "%" + email.toLowerCase() + "%");
}
public static Specification<User> isActive(Boolean active) {
return (root, query, cb) ->
active == null ? null :
cb.equal(root.get("active"), active);
}
public static Specification<User> createdAfter(LocalDate date) {
return (root, query, cb) ->
date == null ? null :
cb.greaterThanOrEqualTo(root.get("createdAt"), date.atStartOfDay());
}
}
// Usage
@GetMapping("/users")
public ResponseEntity<Page<UserResponse>> getUsers(
@RequestParam(required = false) String name,
@RequestParam(required = false) String email,
@RequestParam(required = false) Boolean active,
@RequestParam(required = false) LocalDate createdAfter,
Pageable pageable) {
Specification<User> spec = Specification.where(UserSpecifications.hasName(name))
.and(UserSpecifications.hasEmail(email))
.and(UserSpecifications.isActive(active))
.and(UserSpecifications.createdAfter(createdAfter));
Page<User> pageResult = userService.findAll(spec, pageable);
return ResponseEntity.ok(pageResult.map(this::toResponse));
}
```
### Date Range Filtering
```java
@GetMapping("/orders")
public ResponseEntity<Page<OrderResponse>> getOrders(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate,
Pageable pageable) {
Specification<Order> spec = Specification.where(null);
if (startDate != null) {
spec = spec.and((root, query, cb) ->
cb.greaterThanOrEqualTo(root.get("createdAt"), startDate.atStartOfDay()));
}
if (endDate != null) {
spec = spec.and((root, query, cb) ->
cb.lessThanOrEqualTo(root.get("createdAt"), endDate.atEndOfDay()));
}
Page<Order> pageResult = orderService.findAll(spec, pageable);
return ResponseEntity.ok(pageResult.map(this::toResponse));
}
```
## Advanced Filtering
### Filter DTO Pattern
```java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserFilter {
private String name;
private String email;
private Boolean active;
private LocalDate createdAfter;
private LocalDate createdBefore;
private List<Long> roleIds;
public Specification<User> toSpecification() {
Specification<User> spec = Specification.where(null);
if (name != null && !name.isEmpty()) {
spec = spec.and(hasName(name));
}
if (email != null && !email.isEmpty()) {
spec = spec.and(hasEmail(email));
}
if (active != null) {
spec = spec.and(isActive(active));
}
if (createdAfter != null) {
spec = spec.and(createdAfter(createdAfter));
}
if (createdBefore != null) {
spec = spec.and(createdBefore(createdBefore));
}
if (roleIds != null && !roleIds.isEmpty()) {
spec = spec.and(hasRoles(roleIds));
}
return spec;
}
}
// Controller
@GetMapping("/users")
public ResponseEntity<Page<UserResponse>> getUsers(
UserFilter filter,
Pageable pageable) {
Specification<User> spec = filter.toSpecification();
Page<User> pageResult = userService.findAll(spec, pageable);
return ResponseEntity.ok(pageResult.map(this::toResponse));
}
```
## Link Headers for Pagination
```java
@GetMapping("/users")
public ResponseEntity<Page<UserResponse>> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
Pageable pageable) {
Page<UserResponse> pageResult = userService.findAll(pageable);
HttpHeaders headers = new HttpHeaders();
headers.add("X-Total-Count", String.valueOf(pageResult.getTotalElements()));
headers.add("X-Total-Pages", String.valueOf(pageResult.getTotalPages()));
headers.add("X-Page-Number", String.valueOf(pageResult.getNumber()));
headers.add("X-Page-Size", String.valueOf(pageResult.getSize()));
// Link headers for pagination
if (pageResult.hasNext()) {
headers.add("Link", buildLinkHeader(pageResult.getNumber() + 1, size));
}
if (pageResult.hasPrevious()) {
headers.add("Link", buildLinkHeader(pageResult.getNumber() - 1, size));
}
return ResponseEntity.ok()
.headers(headers)
.body(pageResult);
}
private String buildLinkHeader(int page, int size) {
return String.format("<%s/api/users?page=%d&size=%d>; rel=\"next\"",
baseUrl, page, size);
}
```
## Performance Considerations
### Database Optimization
```java
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
@Transactional(readOnly = true)
public Page<UserResponse> findAll(Specification<User> spec, Pageable pageable) {
// Use projection to only fetch needed fields
Page<User> page = userRepository.findAll(spec, pageable);
return page.stream()
.map(this::toResponse)
.collect(Collectors.toList());
}
}
```
### Cache Pagination Results
```java
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final CacheManager cacheManager;
public Page<UserResponse> findAll(Specification<User> spec, Pageable pageable) {
String cacheKey = "users:" + spec.hashCode() + ":" + pageable.hashCode();
return cacheManager.getCache("users").get(cacheKey, () -> {
Page<User> page = userRepository.findAll(spec, pageable);
return page.map(this::toResponse);
});
}
}
```

View File

@@ -0,0 +1,376 @@
# Spring Boot REST API Standards - References
Complete reference for REST API development with Spring Boot.
## HTTP Methods and Status Codes Reference
### HTTP Methods
| Method | Idempotent | Safe | Purpose | Typical Status |
|--------|-----------|------|---------|----------------|
| GET | Yes | Yes | Retrieve resource | 200, 304, 404 |
| POST | No | No | Create resource | 201, 400, 409 |
| PUT | Yes | No | Replace resource | 200, 204, 404 |
| PATCH | No | No | Partial update | 200, 204, 400 |
| DELETE | Yes | No | Remove resource | 204, 404 |
| HEAD | Yes | Yes | Like GET, no body | 200, 304, 404 |
| OPTIONS | Yes | Yes | Describe communication options | 200 |
### HTTP Status Codes
**2xx Success:**
- `200 OK` - Successful GET/PUT/PATCH
- `201 Created` - Successful POST (include Location header)
- `202 Accepted` - Async processing accepted
- `204 No Content` - Successful DELETE or POST with no content
- `206 Partial Content` - Range request successful
**3xx Redirection:**
- `301 Moved Permanently` - Resource permanently moved
- `304 Not Modified` - Cache valid, use local copy
- `307 Temporary Redirect` - Temporary redirect
**4xx Client Errors:**
- `400 Bad Request` - Invalid format or parameters
- `401 Unauthorized` - Authentication required
- `403 Forbidden` - Authenticated but not authorized
- `404 Not Found` - Resource doesn't exist
- `409 Conflict` - Constraint violation or conflict
- `422 Unprocessable Entity` - Validation failed (semantic error)
**5xx Server Errors:**
- `500 Internal Server Error` - Unexpected server error
- `502 Bad Gateway` - External service unavailable
- `503 Service Unavailable` - Server temporarily down
## Spring Web Annotations Reference
### Request Mapping Annotations
```java
@RestController // Combines @Controller + @ResponseBody
@RequestMapping("/api") // Base URL path
@GetMapping // GET requests
@PostMapping // POST requests
@PutMapping // PUT requests
@PatchMapping // PATCH requests
@DeleteMapping // DELETE requests
```
### Parameter Binding Annotations
```java
@PathVariable // URL path variable /{id}
@RequestParam // Query string parameter ?page=0
@RequestParam(required=false) // Optional parameter
@RequestParam(defaultValue="10") // Default value
@RequestBody // Request body JSON/XML
@RequestHeader // HTTP header value
@CookieValue // Cookie value
@MatrixVariable // Matrix variable ;color=red
@Valid // Enable validation
```
### Response Annotations
```java
@ResponseBody // Serialize to response body
@ResponseStatus(status=HttpStatus.CREATED) // HTTP status
ResponseEntity<T> // Full response control
ResponseEntity.ok(body) // 200 OK
ResponseEntity.created(uri).body(body) // 201 Created
ResponseEntity.noContent().build() // 204 No Content
ResponseEntity.notFound().build() // 404 Not Found
```
## DTO Patterns Reference
### Request DTO (using Records)
```java
public record CreateProductRequest(
@NotBlank(message = "Name required") String name,
@NotNull @DecimalMin("0.01") BigDecimal price,
String description,
@NotNull @Min(0) Integer stock
) {}
```
### Response DTO (using Records)
```java
public record ProductResponse(
Long id,
String name,
BigDecimal price,
Integer stock,
LocalDateTime createdAt,
LocalDateTime updatedAt
) {}
```
### Update DTO
```java
public record UpdateProductRequest(
@NotBlank String name,
@NotNull @DecimalMin("0.01") BigDecimal price,
String description
) {}
```
## Validation Annotations Reference
### Common Constraints
```java
@NotNull // Cannot be null
@NotEmpty // Collection/String cannot be empty
@NotBlank // String cannot be null/blank
@Size(min=1, max=255) // Length validation
@Min(value=1) // Minimum numeric value
@Max(value=100) // Maximum numeric value
@Positive // Must be positive
@Negative // Must be negative
@Email // Valid email format
@Pattern(regexp="...") // Regex validation
@Future // Date must be future
@Past // Date must be past
@Digits(integer=5, fraction=2) // Numeric precision
@DecimalMin("0.01") // Decimal minimum
@DecimalMax("9999.99") // Decimal maximum
```
### Custom Validation
```java
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueEmailValidator.class)
public @interface UniqueEmail {
String message() default "Email already exists";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {
@Autowired
private UserRepository repository;
@Override
public boolean isValid(String email, ConstraintValidatorContext context) {
return !repository.existsByEmail(email);
}
}
```
## Pagination Reference
### Pageable Request Building
```java
// Basic pagination
Pageable pageable = PageRequest.of(page, size);
// With sorting
Sort sort = Sort.by("createdAt").descending();
Pageable pageable = PageRequest.of(page, size, sort);
// Multiple sort fields
Sort sort = Sort.by("status").ascending()
.and(Sort.by("createdAt").descending());
Pageable pageable = PageRequest.of(page, size, sort);
```
### Pagination Response Format
```json
{
"content": [
{ "id": 1, "name": "Product 1" },
{ "id": 2, "name": "Product 2" }
],
"pageable": {
"offset": 0,
"pageNumber": 0,
"pageSize": 20,
"paged": true
},
"totalElements": 100,
"totalPages": 5,
"last": false,
"size": 20,
"number": 0,
"numberOfElements": 20,
"first": true,
"empty": false
}
```
## Error Response Format
### Standardized Error Response
```json
{
"status": 400,
"error": "Bad Request",
"message": "Validation failed: name: Name is required",
"path": "/api/v1/products",
"timestamp": "2024-01-15T10:30:00Z"
}
```
### Global Exception Handler Pattern
```java
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(
MethodArgumentNotValidException ex) {
// Handle validation errors
}
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(
ResourceNotFoundException ex) {
// Handle not found
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
// Handle generic exceptions
}
}
```
## Content Negotiation
### Accept Header Examples
```
Accept: application/json # JSON response
Accept: application/xml # XML response
Accept: application/vnd.api+json # JSON:API standard
Accept: text/csv # CSV response
```
### Controller Implementation
```java
@GetMapping(produces = {MediaType.APPLICATION_JSON_VALUE, "application/xml"})
public ResponseEntity<ProductResponse> getProduct(@PathVariable Long id) {
// Supports both JSON and XML
return ResponseEntity.ok(productService.findById(id));
}
```
## Pagination Best Practices
```java
// Limit maximum page size
@GetMapping
public ResponseEntity<Page<ProductResponse>> getAll(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20")
@Max(value = 100, message = "Max page size is 100") int size) {
Pageable pageable = PageRequest.of(page, size);
return ResponseEntity.ok(productService.findAll(pageable));
}
```
## Maven Dependencies
```xml
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Data JPA (for Pageable) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
```
## Testing REST APIs
### MockMvc Testing
```java
@SpringBootTest
@AutoConfigureMockMvc
class ProductControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void shouldCreateProduct() throws Exception {
mockMvc.perform(post("/api/v1/products")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"Test\",\"price\":10.00}"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").exists());
}
}
```
### TestRestTemplate Testing
```java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ProductIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void shouldFetchProduct() {
ResponseEntity<ProductResponse> response = restTemplate.getForEntity(
"/api/v1/products/1", ProductResponse.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
}
```
## Related Skills
- **spring-boot-crud-patterns/SKILL.md** - CRUD operations following REST principles
- **spring-boot-dependency-injection/SKILL.md** - Dependency injection in controllers
- **spring-boot-test-patterns/SKILL.md** - Testing REST APIs
- **spring-boot-exception-handling/SKILL.md** - Global error handling
## External Resources
### Official Documentation
- [Spring Web MVC Documentation](https://docs.spring.io/spring-framework/reference/web/webmvc.html)
- [Spring REST Documentation](https://spring.io/guides/gs/rest-service/)
- [REST API Best Practices](https://restfulapi.net/)
### Related Standards
- [JSON:API Specification](https://jsonapi.org/)
- [OpenAPI Specification](https://www.openapis.org/)
- [RFC 7231 - HTTP Semantics](https://tools.ietf.org/html/rfc7231)
### Books
- "RESTful Web Services" by Leonard Richardson & Sam Ruby
- "Spring in Action" (latest edition)

View File

@@ -0,0 +1,521 @@
# Security Headers and CORS Configuration
## Security Headers Configuration
### Basic Security Headers
```java
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; " +
"script-src 'self' 'unsafe-inline'; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data:; " +
"font-src 'self';")
.reportOnly(false))
.frameOptions(frame -> frame
.sameOrigin()
.deny()) // Use sameOrigin() for same-origin iframes, deny() to completely block
.httpStrictTransportSecurity(hsts -> hsts
.maxAgeInSeconds(31536000) // 1 year
.includeSubDomains(true)
.preload(true))
.xssProtection(xss -> xss
.headerValue(XssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK))
.contentTypeOptions(contentTypeOptions -> contentTypeOptions
.and())
)
.cors(cors -> cors
.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/**").authenticated()
.anyRequest().permitAll()
);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(List.of("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "X-Requested-With"));
configuration.setExposedHeaders(Arrays.asList("X-Total-Count", "X-Content-Type-Options"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
```
### Enhanced Security Configuration
```java
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class EnhancedSecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/**")
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; " +
"script-src 'self 'unsafe-inline' 'unsafe-eval'; " +
"style-src 'self 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"font-src 'self'; " +
"connect-src 'self' https:; " +
"frame-src 'none'; " +
"object-src 'none';"))
.frameOptions(frameOptions -> frameOptions.sameOrigin())
.httpStrictTransportSecurity(hsts -> hsts
.maxAgeInSeconds(31536000)
.includeSubDomains(true)
.preload(true)
.includeSubDomains(true))
.permissionsPolicy(permissionsPolicy -> permissionsPolicy
.add("camera", "()")
.add("geolocation", "()")
.add("microphone", "()")
.add("payment", "()"))
.referrerPolicy(referrerPolicy -> referrerPolicy.noReferrer())
.and())
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf
.ignoringRequestMatchers("/api/auth/**")
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.GET, "/api/users/**").hasAnyRole("USER", "ADMIN")
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
// Allowed origins (consider restricting to specific domains in production)
configuration.setAllowedOriginPatterns(List.of("https://yourdomain.com", "https://app.yourdomain.com"));
// Allowed methods
configuration.setAllowedMethods(Arrays.asList(
"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"
));
// Allowed headers
configuration.setAllowedHeaders(Arrays.asList(
"Authorization",
"Content-Type",
"Accept",
"X-Requested-With",
"X-Content-Type-Options",
"X-Total-Count",
"Cache-Control"
));
// Exposed headers to client
configuration.setExposedHeaders(Arrays.asList(
"X-Total-Count",
"X-Content-Type-Options",
"Cache-Control"
));
// Allow credentials
configuration.setAllowCredentials(true);
// Cache preflight requests for 1 hour
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", configuration);
return source;
}
}
```
## Content Security Policy (CSP)
### Basic CSP Configuration
```java
@Configuration
public class ContentSecurityPolicyConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://yourdomain.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.exposedHeaders("X-Total-Count")
.allowCredentials(true)
.maxAge(3600);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new ContentSecurityPolicyInterceptor());
}
};
}
}
@Component
public class ContentSecurityPolicyInterceptor implements HandlerInterceptor {
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) throws Exception {
response.setHeader("Content-Security-Policy",
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline'; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data:; " +
"font-src 'self'; " +
"connect-src 'self'; " +
"frame-src 'none'; " +
"object-src 'none';");
response.setHeader("X-Content-Type-Options", "nosniff");
response.setHeader("X-Frame-Options", "DENY");
response.setHeader("X-XSS-Protection", "1; mode=block");
}
}
```
### Advanced CSP with Nonce
```java
@Component
@RequiredArgsConstructor
public class SecurityHeadersFilter extends OncePerRequestFilter {
private final AtomicLong nonceCounter = new AtomicLong(0);
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// Generate nonce for each request
String nonce = String.valueOf(nonceCounter.incrementAndGet());
// Set CSP header with nonce for inline scripts
response.setHeader("Content-Security-Policy",
"default-src 'self'; " +
"script-src 'self' 'nonce-" + nonce + "'; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data:; " +
"font-src 'self'; " +
"connect-src 'self'; " +
"frame-src 'none'; " +
"object-src 'none';");
// Add nonce to request attributes for templates
request.setAttribute("cspNonce", nonce);
// Set other security headers
response.setHeader("X-Content-Type-Options", "nosniff");
response.setHeader("X-Frame-Options", "SAMEORIGIN");
response.setHeader("Strict-Transport-Security",
"max-age=31536000; includeSubDomains; preload");
response.setHeader("X-Permitted-Cross-Domain-Policies", "none");
response.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
filterChain.doFilter(request, response);
}
}
```
## CORS Configuration
### Method-level CORS
```java
@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "https://yourdomain.com", methods = {RequestMethod.GET, RequestMethod.POST})
public class UserController {
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
// CORS allowed for GET requests
return ResponseEntity.ok(userService.findAll());
}
@PostMapping
@CrossOrigin(origins = "https://app.yourdomain.com")
public ResponseEntity<User> createUser(@RequestBody User user) {
// CORS allowed with different origin for POST requests
return ResponseEntity.status(HttpStatus.CREATED).body(userService.create(user));
}
}
```
### Dynamic CORS Configuration
```java
@Configuration
public class DynamicCorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// Development configuration
CorsConfiguration devConfig = new CorsConfiguration();
devConfig.setAllowedOriginPatterns(List.of("*"));
devConfig.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
devConfig.setAllowedHeaders(Arrays.asList("*"));
devConfig.setAllowCredentials(true);
source.registerCorsConfiguration("/api/**", devConfig);
// Production configuration - restrict to specific domains
CorsConfiguration prodConfig = new CorsConfiguration();
prodConfig.setAllowedOriginPatterns(List.of(
"https://yourdomain.com",
"https://app.yourdomain.com",
"https://api.yourdomain.com"
));
prodConfig.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
prodConfig.setAllowedHeaders(Arrays.asList(
"Authorization",
"Content-Type",
"Accept",
"X-Requested-With"
));
prodConfig.setExposedHeaders(Arrays.asList("X-Total-Count"));
prodConfig.setAllowCredentials(true);
source.registerCorsConfiguration("/api/**", prodConfig);
return source;
}
}
```
## Security Headers Best Practices
### Essential Headers for Production
1. **Content-Security-Policy**: Mitigates XSS attacks
2. **X-Content-Type-Options**: Prevents MIME type sniffing
3. **X-Frame-Options**: Prevents clickjacking
4. **Strict-Transport-Security**: Enforces HTTPS
5. **X-XSS-Protection**: Legacy browser XSS protection
6. **Referrer-Policy**: Controls referrer information
### CSP Examples by Application Type
#### Blog/Content Site
```java
response.setHeader("Content-Security-Policy",
"default-src 'self'; " +
"script-src 'self' https://cdn.jsdelivr.net; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' https: data:; " +
"font-src 'self'; " +
"connect-src 'self'; " +
"frame-src https://www.youtube.com; " +
"media-src https://www.youtube.com;");
```
#### Single Page Application (SPA)
```java
response.setHeader("Content-Security-Policy",
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data:; " +
"font-src 'self'; " +
"connect-src 'self' wss:; " +
"frame-src 'none'; " +
"object-src 'none';");
```
#### API Only
```java
response.setHeader("Content-Security-Policy",
"default-src 'self'; " +
"connect-src 'self'; " +
"frame-src 'none'; " +
"object-src 'none';");
```
### Security Header Testing
```java
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
class SecurityHeadersTest {
@Autowired
private MockMvc mockMvc;
@Test
void securityHeaders_shouldBeSet() throws Exception {
mockMvc.perform(get("/api/users"))
.andExpect(header().string("Content-Security-Policy", notNullValue()))
.andExpect(header().string("X-Content-Type-Options", "nosniff"))
.andExpect(header().string("X-Frame-Options", notNullValue()))
.andExpect(header().string("Strict-Transport-Security", notNullValue()));
}
}
```
## Rate Limiting
### Basic Rate Limiting
```java
@Component
public class RateLimitingFilter extends OncePerRequestFilter {
private final ConcurrentHashMap<String, RateLimit> rateLimits = new ConcurrentHashMap<>();
private static final long REQUEST_LIMIT = 100;
private static final long TIME_WINDOW = 60_000; // 1 minute
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String clientIp = request.getRemoteAddr();
String path = request.getRequestURI();
String key = clientIp + ":" + path;
RateLimit rateLimit = rateLimits.computeIfAbsent(key, k -> new RateLimit());
synchronized (rateLimit) {
if (System.currentTimeMillis() - rateLimit.resetTime > TIME_WINDOW) {
rateLimit.count = 0;
rateLimit.resetTime = System.currentTimeMillis();
}
if (rateLimit.count >= REQUEST_LIMIT) {
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.getWriter().write("Rate limit exceeded");
return;
}
rateLimit.count++;
}
filterChain.doFilter(request, response);
}
private static class RateLimit {
long count = 0;
long resetTime = System.currentTimeMillis();
}
}
```
## Token-based Authentication Headers
```java
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && jwtTokenProvider.validateToken(jwt)) {
UsernamePasswordAuthenticationToken authentication =
jwtTokenProvider.getAuthentication(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
} catch (Exception ex) {
logger.error("Could not set user authentication in security context", ex);
response.sendError(HttpStatus.UNAUTHORIZED.value(), "Unauthorized");
}
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
```
## WebSocket Security
```java
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketSecurityConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("https://yourdomain.com")
.withSockJS();
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
// Validate token and authenticate
String token = accessor.getFirstNativeHeader("Authorization");
if (!isValidToken(token)) {
throw new UnauthorizedWebSocketException("Invalid token");
}
}
return message;
}
});
}
}
```

View File

@@ -0,0 +1,309 @@
# Spring Web Annotations Reference
## Controller and Mapping Annotations
### @RestController
```java
@RestController
@RequestMapping("/api/users")
public class UserController {
// Returns JSON responses automatically
}
```
### @Controller
```java
@Controller
@RequestMapping("/users")
public class UserController {
// Returns view names for MVC applications
}
```
### @RequestMapping
```java
// Class level
@RequestMapping("/api")
@RequestMapping(path = "/api", method = RequestMethod.GET)
// Method level
@RequestMapping("/users")
@RequestMapping(path = "/users", method = RequestMethod.POST)
```
### HTTP Method Annotations
```java
@GetMapping("/users")
public List<User> getUsers() { ... }
@PostMapping("/users")
public User createUser(@RequestBody User user) { ... }
@PutMapping("/users/{id}")
public User updateUser(@PathVariable Long id, @RequestBody User user) { ... }
@PatchMapping("/users/{id}")
public User patchUser(@PathVariable Long id, @RequestBody User user) { ... }
@DeleteMapping("/users/{id}")
public void deleteUser(@PathVariable Long id) { ... }
@HeadMapping("/users/{id}")
public ResponseEntity<Void> headUser(@PathVariable Long id) { ... }
@OptionsMapping("/users")
public ResponseEntity<Void> optionsUsers() { ... }
```
## Parameter Binding Annotations
### @PathVariable
```java
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) { ... }
// Multiple path variables
@GetMapping("/users/{userId}/orders/{orderId}")
public Order getOrder(@PathVariable Long userId, @PathVariable Long orderId) { ... }
// Custom variable name
@GetMapping("/users/{userId}")
public User getUser(@PathVariable("userId") Long id) { ... }
```
### @RequestParam
```java
@GetMapping("/users")
public List<User> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) String name,
@RequestParam(defaultValue = "createdAt") String sortBy,
@RequestParam(defaultValue = "DESC") String sortDirection) {
// Handle pagination, filtering, and sorting
}
```
### @RequestBody
```java
@PostMapping("/users")
public User createUser(@RequestBody User user) { ... }
// With validation
@PostMapping("/users")
public User createUser(@Valid @RequestBody User user) { ... }
```
### @RequestHeader
```java
@GetMapping("/users")
public List<User> getUsers(@RequestHeader("Authorization") String authHeader) { ... }
// Multiple headers
@PostMapping("/users")
public User createUser(
@RequestBody User user,
@RequestHeader("X-Custom-Header") String customHeader) { ... }
```
### @CookieValue
```java
@GetMapping("/users")
public List<User> getUsers(@CookieValue("JSESSIONID") String sessionId) { ... }
```
### @MatrixVariable
```java
@GetMapping("/users/{id}")
public User getUser(
@PathVariable Long id,
@MatrixVariable(pathVar = "id", required = false) Map<String, String> params) {
// Handle matrix variables: /users/123;name=John;age=30
}
```
## Response Annotations
### @ResponseStatus
```java
@PostMapping("/users")
@ResponseStatus(HttpStatus.CREATED)
public User createUser(@RequestBody User user) { ... }
```
### @ResponseBody
```java
@Controller
public class UserController {
@GetMapping("/users")
@ResponseBody
public List<User> getUsers() { ... }
}
```
### ResponseEntity
```java
@GetMapping("/users/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
return userRepository.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody User user) {
User created = userService.create(user);
return ResponseEntity.status(HttpStatus.CREATED)
.header("Location", "/api/users/" + created.getId())
.body(created);
}
```
## Content Negotiation
### Produces and Consumes
```java
@GetMapping(value = "/users", produces = MediaType.APPLICATION_JSON_VALUE)
public List<User> getUsers() { ... }
@PostMapping(value = "/users", consumes = MediaType.APPLICATION_JSON_VALUE)
public User createUser(@RequestBody User user) { ... }
// Multiple media types
@GetMapping(value = "/users", produces = {
MediaType.APPLICATION_JSON_VALUE,
MediaType.APPLICATION_XML_VALUE
})
public List<User> getUsers() { ... }
```
### @RequestBody with Content-Type
```java
@PostMapping(value = "/users", consumes = "application/json")
public User createUserJson(@RequestBody User user) { ... }
@PostMapping(value = "/users", consumes = "application/xml")
public User createUserXml(@RequestBody User user) { ... }
```
## Validation Annotations
### @Valid
```java
@PostMapping("/users")
public User createUser(@Valid @RequestBody User user) { ... }
// Validates individual parameters
@GetMapping("/users")
public User getUser(
@Valid @Pattern(regexp = "^[a-zA-Z0-9]+$") @PathVariable String id) { ... }
```
### Jakarta Bean Validation Annotations
```java
public class UserRequest {
@NotBlank(message = "Name is required")
private String name;
@Email(message = "Valid email required")
private String email;
@Size(min = 8, max = 100, message = "Password must be 8-100 characters")
private String password;
@Min(value = 18, message = "Must be at least 18")
@Max(value = 120, message = "Invalid age")
private Integer age;
@Pattern(regexp = "^[A-Z][a-z]+$", message = "Invalid name format")
private String firstName;
@NotEmpty(message = "At least one role required")
private Set<String> roles = new HashSet<>();
@Future(message = "Date must be in the future")
private LocalDate futureDate;
@Past(message = "Date must be in the past")
private LocalDate birthDate;
@Positive(message = "Value must be positive")
private Double positiveValue;
@PositiveOrZero(message = "Value must be positive or zero")
private Double nonNegativeValue;
}
```
## Specialized Annotations
### @RestControllerAdvice
```java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(
MethodArgumentNotValidException ex) {
// Handle validation errors globally
}
}
```
### @ExceptionHandler
```java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(NoHandlerFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(NoHandlerFoundException ex) {
return ResponseEntity.notFound().build();
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
return ResponseEntity.internalServerError().build();
}
}
```
### @CrossOrigin
```java
@CrossOrigin(origins = "http://localhost:3000")
@RestController
@RequestMapping("/api/users")
public class UserController {
// Enable CORS for specific origin
}
// Or at method level
@CrossOrigin(origins = "*", methods = {RequestMethod.GET, RequestMethod.POST})
@GetMapping("/users")
public List<User> getUsers() { ... }
```
## Async Processing
### @Async
```java
@Service
public class AsyncService {
@Async
public CompletableFuture<User> processUser(User user) {
// Long-running operation
return CompletableFuture.completedFuture(processedUser);
}
}
@RestController
public class UserController {
@GetMapping("/users/{id}/async")
public CompletableFuture<ResponseEntity<User>> getUserAsync(@PathVariable Long id) {
return userService.processUser(id)
.thenApply(ResponseEntity::ok)
.exceptionally(ex -> ResponseEntity.notFound().build());
}
}
```