
Spring Boot Rest Api Standards
- 1.9k installs
- 311 repo stars
- Updated June 22, 2026
- giuseppe-trisciuoglio/developer-kit
spring-boot-rest-api-standards is an agent skill for Provides REST API design standards and best practices for Spring Boot projects. Use when creating or reviewing REST endpoints, DTOs, error handling, pagination, securi
About
Provides 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. The spring-boot-rest-api-standards skill documents workflows and patterns from the repository SKILL.md. --- name: spring-boot-rest-api-standards description: Provides 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. allowed-tools: Read, Write, Edit, Bash, Glob, Grep --- # Spring Boot REST API Standards ## Overview REST API design standards for Spring Boot covering URL design, HTTP methods, status codes, DTOs, validation, error handling, pagination, and security headers. ## When to Use - Creating REST endpoints and API routes - Designing DTOs and API contracts - Implementing error handling and validation - Setting up pagination and filtering - Configuring security headers and CORS - Reviewing REST API architecture ## Instructions ### To Build RESTful API Endpoints Follow these steps to create well-designed REST API end.
- Spring Boot REST API Standards
- Creating REST endpoints and API routes
- Designing DTOs and API contracts
- Implementing error handling and validation
- Setting up pagination and filtering
Spring Boot Rest Api Standards by the numbers
- 1,947 all-time installs (skills.sh)
- +61 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #384 of 2,184 Testing & QA skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
spring-boot-rest-api-standards capabilities & compatibility
- Capabilities
- spring boot rest api standards · creating rest endpoints and api routes · designing dtos and api contracts · implementing error handling and validation · setting up pagination and filtering
- Use cases
- documentation
What spring-boot-rest-api-standards says it does
--- name: spring-boot-rest-api-standards description: Provides 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.
**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.
**Configure Pagination** - Use Pageable for large datasets - Include page, size, sort parameters - Return metadata with total elements, totalPages, etc.
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill spring-boot-rest-api-standardsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.9k |
|---|---|
| repo stars | ★ 311 |
| Security audit | 3 / 3 scanners passed |
| Last updated | June 22, 2026 |
| Repository | giuseppe-trisciuoglio/developer-kit ↗ |
What problem does spring-boot-rest-api-standards solve for developers using the documented workflows?
Provides 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 architect
Who is it for?
Developers working with spring-boot-rest-api-standards patterns described in the skill documentation.
Skip if: Skip when docs are empty or the task is outside the skill documented scope.
When should I use this skill?
Provides 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 architect
What you get
Grounded guidance and workflows from SKILL.md for spring-boot-rest-api-standards.
- Layered Java package structure
- REST controllers and DTO records
- Domain entities and repository ports
Files
Spring Boot REST API Standards
Overview
REST API design standards for Spring Boot covering URL design, HTTP methods, status codes, DTOs, validation, error handling, pagination, and security headers.
When to Use
- Creating REST endpoints and API routes
- Designing DTOs and API contracts
- Implementing error handling and validation
- Setting up pagination and filtering
- Configuring security headers and CORS
- Reviewing REST API architecture
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
@Validannotation on@RequestBodyparameters - Apply validation constraints (
@NotBlank,@Email,@Size, etc.) - Handle validation errors with
MethodArgumentNotValidException
6. Set Up Error Handling
- Use
@RestControllerAdvicefor global exception handling - Return standardized error responses with status, error, message, and timestamp
- Use
ResponseStatusExceptionfor 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
Validation checkpoints:
- After step 1-2: Verify URL structure follows REST conventions (/users not /getUsers)
- After step 3: Test each endpoint returns correct status codes
- After step 4-5: Validate DTOs with curl or HTTPie before proceeding
- After step 6: Confirm error responses match standardized format
Examples
Basic CRUD Controller
@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
// 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
@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
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
}2. Prefer Immutable DTOs (Java Records or @Value)
public record UserResponse(Long id, String name, String email) {}3. Implement Proper Transaction Management
@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); }
}Constraints and Warnings
1. Never expose entities directly - Use DTOs to separate API contracts from domain models 2. Follow REST conventions - Use nouns for resources (/users), correct HTTP methods, plural names, proper status codes 3. Handle all exceptions globally - Use @RestControllerAdvice, never let raw exceptions bubble up 4. Always paginate large result sets - Prevent performance issues and DDoS vulnerabilities 5. Validate all input data - Use Jakarta validation annotations on request DTOs 6. Never expose sensitive data - Don't log or expose passwords, tokens, PII
References
- See
references/directory for comprehensive reference material including HTTP status codes, Spring annotations, and detailed examples - Refer to the
developer-kit-java:spring-boot-code-review-expertagent for code review guidelines - Review
spring-boot-dependency-injection/SKILL.mdfor dependency injection patterns - Check
../spring-boot-test-patterns/SKILL.mdfor testing REST APIs
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.javaDomain Layer (Clean Architecture)
Domain Entity
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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"));
}
}Spring Boot REST API Examples
Complete CRUD REST API with Validation
Entity with Validation
@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
@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
@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
@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
@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
@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
@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
@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
@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
@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
@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
@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
@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
@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 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
}
}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/PATCH201 Created: Successful POST (include Location header)202 Accepted: Async processing accepted204 No Content: Successful DELETE or POST with no content206 Partial Content: Range request successful
3xx Redirection
301 Moved Permanently: Resource permanently moved304 Not Modified: Cache valid, use local copy307 Temporary Redirect: Temporary redirect
4xx Client Errors
400 Bad Request: Invalid format or parameters401 Unauthorized: Authentication required403 Forbidden: Authenticated but not authorized404 Not Found: Resource doesn't exist409 Conflict: Constraint violation or conflict422 Unprocessable Entity: Validation failed (semantic error)429 Too Many Requests: Rate limit exceeded
5xx Server Errors
500 Internal Server Error: Unexpected server error502 Bad Gateway: External service unavailable503 Service Unavailable: Server temporarily down504 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 ordersQuery 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 formatError Response Format
{
"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"
}Pagination and Filtering Reference
Pagination with Spring Data
Basic Pagination
@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
@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
@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
{
"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
@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
@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
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
@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
@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
@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
@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
@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);
});
}
}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/PATCH201 Created- Successful POST (include Location header)202 Accepted- Async processing accepted204 No Content- Successful DELETE or POST with no content206 Partial Content- Range request successful
3xx Redirection:
301 Moved Permanently- Resource permanently moved304 Not Modified- Cache valid, use local copy307 Temporary Redirect- Temporary redirect
4xx Client Errors:
400 Bad Request- Invalid format or parameters401 Unauthorized- Authentication required403 Forbidden- Authenticated but not authorized404 Not Found- Resource doesn't exist409 Conflict- Constraint violation or conflict422 Unprocessable Entity- Validation failed (semantic error)
5xx Server Errors:
500 Internal Server Error- Unexpected server error502 Bad Gateway- External service unavailable503 Service Unavailable- Server temporarily down
Spring Web Annotations Reference
Request Mapping Annotations
@RestController // Combines @Controller + @ResponseBody
@RequestMapping("/api") // Base URL path
@GetMapping // GET requests
@PostMapping // POST requests
@PutMapping // PUT requests
@PatchMapping // PATCH requests
@DeleteMapping // DELETE requestsParameter Binding Annotations
@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 validationResponse Annotations
@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 FoundDTO Patterns Reference
Request DTO (using Records)
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)
public record ProductResponse(
Long id,
String name,
BigDecimal price,
Integer stock,
LocalDateTime createdAt,
LocalDateTime updatedAt
) {}Update DTO
public record UpdateProductRequest(
@NotBlank String name,
@NotNull @DecimalMin("0.01") BigDecimal price,
String description
) {}Validation Annotations Reference
Common Constraints
@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 maximumCustom Validation
@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
// 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
{
"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
{
"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
@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 responseController Implementation
@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
// 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
<!-- 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
@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
@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
Related Standards
Books
- "RESTful Web Services" by Leonard Richardson & Sam Ruby
- "Spring in Action" (latest edition)
Security Headers and CORS Configuration
Security Headers Configuration
Basic Security Headers
@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
@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
@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
@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
@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
@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
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)
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
response.setHeader("Content-Security-Policy",
"default-src 'self'; " +
"connect-src 'self'; " +
"frame-src 'none'; " +
"object-src 'none';");Security Header Testing
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
@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
@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
@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;
}
});
}
}Spring Web Annotations Reference
Controller and Mapping Annotations
@RestController
@RestController
@RequestMapping("/api/users")
public class UserController {
// Returns JSON responses automatically
}@Controller
@Controller
@RequestMapping("/users")
public class UserController {
// Returns view names for MVC applications
}@RequestMapping
// Class level
@RequestMapping("/api")
@RequestMapping(path = "/api", method = RequestMethod.GET)
// Method level
@RequestMapping("/users")
@RequestMapping(path = "/users", method = RequestMethod.POST)HTTP Method Annotations
@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
@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
@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
@PostMapping("/users")
public User createUser(@RequestBody User user) { ... }
// With validation
@PostMapping("/users")
public User createUser(@Valid @RequestBody User user) { ... }@RequestHeader
@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
@GetMapping("/users")
public List<User> getUsers(@CookieValue("JSESSIONID") String sessionId) { ... }@MatrixVariable
@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
@PostMapping("/users")
@ResponseStatus(HttpStatus.CREATED)
public User createUser(@RequestBody User user) { ... }@ResponseBody
@Controller
public class UserController {
@GetMapping("/users")
@ResponseBody
public List<User> getUsers() { ... }
}ResponseEntity
@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
@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
@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
@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
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
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(
MethodArgumentNotValidException ex) {
// Handle validation errors globally
}
}@ExceptionHandler
@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
@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
@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());
}
}Related skills
Forks & variants (1)
Spring Boot Rest Api Standards has 1 known copy in the catalog totaling 21 installs. They canonicalize to this original listing.
- giuseppe-trisciuoglio - 21 installs
FAQ
Who is Spring Boot Rest Api Standards for?
Developers and software engineers working with spring-boot-rest-api-standards patterns from the skill documentation.
When should I use Spring Boot Rest Api Standards?
Provides 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.
Is Spring Boot Rest Api Standards safe to install?
Review the Security Audits panel on this page before installing in production.