internal/skills/content/spring-boot-java/SKILL.md
Spring Boot (Java) framework guardrails, patterns, and best practices. Use when working with Spring Boot Java projects, or when the user mentions Spring Boot. Provides JPA, Security, REST, WebFlux, and enterprise Java guidelines.
npx skillsauth add ar4mirez/samuel spring-boot-javaInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Applies to: Spring Boot 3.x, Java 17+, REST APIs, Microservices, Enterprise Applications
@RequiredArgsConstructor or explicit constructors; never field injectionapplication.yml with profile-specific overridesddl-auto=update in production@Transactional(readOnly = true) at service class level, @Transactional on write methodsspring.jpa.open-in-view=false to prevent lazy loading surprisesProblemDetail (RFC 7807) for all error responses@RequiredArgsConstructor (Lombok) or explicit constructors@Autowired on fields@Bean methods in @Configuration classes for third-party types@Configuration class per concern (SecurityConfig, WebConfig, CacheConfig)/api/v1/resources201 Created with Location header for POST204 No Content for DELETE@Valid and Jakarta Bean Validation@PageableDefault for list endpoints; never return unbounded collections@Operation, @Tag, @ApiResponse)application.yml over application.propertiesapplication-dev.yml, application-prod.yml${DB_PASSWORD:default}server.error.include-message=never in production profilesmyproject/
├── src/main/java/com/example/myproject/
│ ├── MyProjectApplication.java # @SpringBootApplication entry point
│ ├── config/
│ │ ├── SecurityConfig.java # Spring Security filter chain
│ │ └── WebConfig.java # CORS, interceptors, converters
│ ├── controller/
│ │ └── UserController.java # REST endpoints
│ ├── service/
│ │ ├── UserService.java # Interface
│ │ └── impl/
│ │ └── UserServiceImpl.java # Implementation
│ ├── repository/
│ │ └── UserRepository.java # JpaRepository interface
│ ├── model/
│ │ ├── entity/
│ │ │ └── User.java # JPA entity
│ │ └── dto/
│ │ ├── UserRequest.java # Input record with validation
│ │ └── UserResponse.java # Output record
│ ├── exception/
│ │ ├── GlobalExceptionHandler.java # @RestControllerAdvice
│ │ └── ResourceNotFoundException.java
│ └── mapper/
│ └── UserMapper.java # MapStruct interface
├── src/main/resources/
│ ├── application.yml
│ ├── application-dev.yml
│ ├── application-prod.yml
│ └── db/migration/
│ └── V1__create_users_table.sql # Flyway migration
├── src/test/java/com/example/myproject/
│ ├── controller/
│ │ └── UserControllerTest.java # MockMvc tests
│ ├── service/
│ │ └── UserServiceTest.java # Mockito unit tests
│ └── integration/
│ └── UserIntegrationTest.java # Testcontainers
├── pom.xml
└── Dockerfile
mapper/ packageentity/ and dto/ under model/ to reinforce the boundary@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
@Tag(name = "Users", description = "User management APIs")
public class UserController {
private final UserService userService;
@PostMapping
@Operation(summary = "Create a new user")
@ApiResponse(responseCode = "201", description = "User created")
@ApiResponse(responseCode = "409", description = "Email conflict")
public ResponseEntity<UserResponse> createUser(
@Valid @RequestBody UserRequest request) {
UserResponse response = userService.createUser(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
@GetMapping("/{id}")
@Operation(summary = "Get user by ID")
public ResponseEntity<UserResponse> getUserById(@PathVariable Long id) {
return ResponseEntity.ok(userService.getUserById(id));
}
@GetMapping
@Operation(summary = "List users with pagination")
public ResponseEntity<Page<UserResponse>> getAllUsers(
@PageableDefault(size = 20, sort = "id") Pageable pageable) {
return ResponseEntity.ok(userService.getAllUsers(pageable));
}
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "Delete user (admin only)")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional(readOnly = true)
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final UserMapper userMapper;
private final PasswordEncoder passwordEncoder;
@Override
@Transactional
public UserResponse createUser(UserRequest request) {
if (userRepository.existsByEmail(request.email())) {
throw new DuplicateResourceException("User", "email", request.email());
}
User user = userMapper.toEntity(request);
user.setPassword(passwordEncoder.encode(request.password()));
User saved = userRepository.save(user);
return userMapper.toResponse(saved);
}
@Override
public UserResponse getUserById(Long id) {
return userRepository.findById(id)
.map(userMapper::toResponse)
.orElseThrow(() -> new ResourceNotFoundException("User", "id", id));
}
@Override
public Page<UserResponse> getAllUsers(Pageable pageable) {
return userRepository.findAll(pageable).map(userMapper::toResponse);
}
@Override
@Transactional
public void deleteUser(Long id) {
if (!userRepository.existsById(id)) {
throw new ResourceNotFoundException("User", "id", id);
}
userRepository.deleteById(id);
}
}
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
List<User> findByActiveTrue();
Page<User> findByRole(User.Role role, Pageable pageable);
@Query("SELECT u FROM User u WHERE u.active = true AND u.role = :role")
List<User> findActiveUsersByRole(@Param("role") User.Role role);
}
@Query with JPQL for joins and complex filtersOptional for single-entity lookupsPage/Slice for paginated results@Entity
@Table(name = "users")
@Getter @Setter
@NoArgsConstructor @AllArgsConstructor @Builder
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String password;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
@Builder.Default
private Role role = Role.USER;
@CreationTimestamp
@Column(updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
private LocalDateTime updatedAt;
public enum Role { USER, ADMIN }
}
// Request DTO
@Builder
public record UserRequest(
@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
String email,
@NotBlank(message = "Password is required")
@Size(min = 8, max = 100)
String password,
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100)
String name
) {}
// Response DTO
@Builder
public record UserResponse(
Long id,
String email,
String name,
String role,
LocalDateTime createdAt
) {}
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ProblemDetail handleNotFound(ResourceNotFoundException ex) {
log.warn("Resource not found: {}", ex.getMessage());
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.NOT_FOUND, ex.getMessage());
problem.setTitle("Resource Not Found");
problem.setProperty("timestamp", Instant.now());
return problem;
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach(error -> {
String field = ((FieldError) error).getField();
errors.put(field, error.getDefaultMessage());
});
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.BAD_REQUEST, "Validation failed");
problem.setTitle("Validation Error");
problem.setProperty("timestamp", Instant.now());
problem.setProperty("errors", errors);
return problem;
}
@ExceptionHandler(Exception.class)
public ProblemDetail handleUnexpected(Exception ex) {
log.error("Unexpected error", ex);
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.INTERNAL_SERVER_ERROR, "An unexpected error occurred");
problem.setTitle("Internal Server Error");
problem.setProperty("timestamp", Instant.now());
return problem;
}
}
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // enables @PreAuthorize for method-level access control
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable) // disable for stateless REST APIs
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api-docs/**", "/swagger-ui/**").permitAll()
.requestMatchers("/actuator/health", "/actuator/info").permitAll()
.requestMatchers(HttpMethod.POST, "/api/v1/users").permitAll()
.requestMatchers(HttpMethod.DELETE, "/api/v1/users/**").hasRole("ADMIN")
.anyRequest().authenticated())
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // cost factor 12: security/performance balance
}
}
@ExtendWith(MockitoExtension.class)
@DisplayName("UserService")
class UserServiceTest {
@Mock private UserRepository userRepository;
@Mock private UserMapper userMapper;
@Mock private PasswordEncoder passwordEncoder;
@InjectMocks private UserServiceImpl userService;
@Test
@DisplayName("should create user with valid data")
void shouldCreateUser() {
when(userRepository.existsByEmail("[email protected]")).thenReturn(false);
when(userMapper.toEntity(any())).thenReturn(new User());
when(userRepository.save(any())).thenReturn(new User());
when(userMapper.toResponse(any())).thenReturn(
new UserResponse(1L, "[email protected]", "Test", "USER", null));
UserResponse result = userService.createUser(
new UserRequest("[email protected]", "Pass123!", "Test"));
assertThat(result.email()).isEqualTo("[email protected]");
verify(userRepository).save(any(User.class));
}
}
@SpringBootTest
@AutoConfigureMockMvc
@Testcontainers
class UserIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15-alpine");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired private MockMvc mockMvc;
@Autowired private ObjectMapper objectMapper;
@Test
void shouldCreateUserViaApi() throws Exception {
var request = new UserRequest("[email protected]", "Pass123!", "Test");
mockMvc.perform(post("/api/v1/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.email").value("[email protected]"));
}
}
# Create project via Spring Initializr
# https://start.spring.io/
# Build
./mvnw clean package
# Run
./mvnw spring-boot:run
# Run with profile
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev
# Test
./mvnw test
# Test with coverage
./mvnw verify
# Format (with spotless plugin)
./mvnw spotless:apply
# Static analysis
./mvnw checkstyle:check
# Build Docker image (Spring Boot Buildpacks)
./mvnw spring-boot:build-image
# Native executable (GraalVM)
./mvnw -Pnative native:compile
| Do | Do Not |
|----|--------|
| Constructor injection (@RequiredArgsConstructor) | @Autowired on fields |
| @Transactional(readOnly = true) at class level | @Transactional without readOnly at class level |
| DTOs (records) for API input/output | Expose JPA entities in responses |
| MapStruct for entity-DTO mapping | Manual mapping boilerplate |
| Pagination for all list endpoints | Unbounded collection returns |
| ProblemDetail (RFC 7807) errors | Custom error formats |
| Flyway/Liquibase for migrations | ddl-auto=update in production |
| Testcontainers for integration tests | H2 as production substitute |
| open-in-view=false | Lazy loading outside transactions |
| JPA batch inserts (hibernate.jdbc.batch_size) | Individual saves in loops |
For detailed patterns and advanced configurations, see:
development
Zig language guardrails, patterns, and best practices for AI-assisted development. Use when working with Zig files (.zig), build.zig, or when the user mentions Zig. Provides comptime patterns, allocator conventions, C interop guidelines, and testing standards specific to this project's coding standards.
tools
WordPress framework guardrails, patterns, and best practices for AI-assisted development. Use when working with WordPress projects, or when the user mentions WordPress. Provides theme development, plugin architecture, REST API, blocks, and security guidelines.
tools
Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs. Use when testing web apps, automating browser interactions, or debugging frontend issues.
tools
Suite of tools for creating elaborate, multi-component web applications using modern frontend technologies (React, Tailwind CSS, shadcn/ui). Use for complex projects requiring state management, routing, or shadcn/ui components - not for simple single-file HTML/JSX pages.