toolchains/java/frameworks/spring-boot/SKILL.md
Spring Boot 3.x - Java framework for production-ready applications with dependency injection, REST APIs, data access, security, and actuator monitoring
npx skillsauth add bobmatnyc/claude-mpm-skills spring-bootInstall 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.
Spring Boot is an opinionated Java framework for building production-ready applications with minimal configuration. It provides auto-configuration, embedded servers, and production-ready features like health checks and metrics.
Key Features:
Requirements:
Quick Start:
# Create project from Spring Initializr
curl https://start.spring.io/starter.zip \
-d type=maven-project \
-d language=java \
-d bootVersion=3.2.0 \
-d dependencies=web,data-jpa,postgresql,lombok,actuator \
-d name=myapp \
-o myapp.zip && unzip myapp.zip
# Run the application
cd myapp
./mvnw spring-boot:run
src/
├── main/
│ ├── java/com/example/myapp/
│ │ ├── MyappApplication.java # Main class
│ │ ├── config/ # @Configuration classes
│ │ ├── controller/ # @RestController classes
│ │ ├── service/ # @Service classes
│ │ ├── repository/ # @Repository interfaces
│ │ ├── model/ # Entity classes
│ │ ├── dto/ # Data Transfer Objects
│ │ └── exception/ # Exception handlers
│ └── resources/
│ ├── application.yml # Configuration
│ └── application-{profile}.yml # Profile-specific config
└── test/
└── java/com/example/myapp/ # Test classes
// Main application class
@SpringBootApplication // Combines @Configuration, @EnableAutoConfiguration, @ComponentScan
public class MyappApplication {
public static void main(String[] args) {
SpringApplication.run(MyappApplication.class, args);
}
}
// Constructor injection (recommended)
@Service
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
// @Autowired optional on single constructor (Spring 4.3+)
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
}
// With Lombok
@Service
@RequiredArgsConstructor // Generates constructor for final fields
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
}
// Field injection (avoid in production code)
@Service
public class UserService {
@Autowired
private UserRepository userRepository; // Harder to test
}
@Component // Generic component
@Service // Business logic layer
@Repository // Data access layer (enables exception translation)
@Controller // MVC controller (returns views)
@RestController // REST API controller (returns JSON)
@Configuration // Configuration class with @Bean methods
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
// GET /api/v1/users
@GetMapping
public ResponseEntity<List<UserDto>> getAllUsers() {
return ResponseEntity.ok(userService.findAll());
}
// GET /api/v1/users/{id}
@GetMapping("/{id}")
public ResponseEntity<UserDto> getUserById(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
// POST /api/v1/users
@PostMapping
public ResponseEntity<UserDto> createUser(@Valid @RequestBody CreateUserRequest request) {
UserDto created = userService.create(request);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(created.getId())
.toUri();
return ResponseEntity.created(location).body(created);
}
// PUT /api/v1/users/{id}
@PutMapping("/{id}")
public ResponseEntity<UserDto> updateUser(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
return ResponseEntity.ok(userService.update(id, request));
}
// DELETE /api/v1/users/{id}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.noContent().build();
}
// GET /api/v1/users/[email protected]
@GetMapping("/search")
public ResponseEntity<List<UserDto>> searchUsers(
@RequestParam(required = false) String email,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
return ResponseEntity.ok(userService.search(email, page, size));
}
}
// Request DTO with validation
@Data
public class CreateUserRequest {
@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
private String email;
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100, message = "Name must be 2-100 characters")
private String name;
@NotBlank(message = "Password is required")
@Size(min = 8, message = "Password must be at least 8 characters")
private String password;
}
// Response DTO
@Data
@Builder
public class UserDto {
private Long id;
private String email;
private String name;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public static UserDto fromEntity(User user) {
return UserDto.builder()
.id(user.getId())
.email(user.getEmail())
.name(user.getName())
.createdAt(user.getCreatedAt())
.updatedAt(user.getUpdatedAt())
.build();
}
}
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true) // Default to read-only transactions
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public List<UserDto> findAll() {
return userRepository.findAll().stream()
.map(UserDto::fromEntity)
.collect(Collectors.toList());
}
public Optional<UserDto> findById(Long id) {
return userRepository.findById(id)
.map(UserDto::fromEntity);
}
@Transactional // Read-write transaction
public UserDto create(CreateUserRequest request) {
if (userRepository.existsByEmail(request.getEmail())) {
throw new EmailAlreadyExistsException(request.getEmail());
}
User user = User.builder()
.email(request.getEmail())
.name(request.getName())
.passwordHash(passwordEncoder.encode(request.getPassword()))
.build();
return UserDto.fromEntity(userRepository.save(user));
}
@Transactional
public UserDto update(Long id, UpdateUserRequest request) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
if (request.getName() != null) {
user.setName(request.getName());
}
if (request.getEmail() != null) {
user.setEmail(request.getEmail());
}
return UserDto.fromEntity(userRepository.save(user));
}
@Transactional
public void delete(Long id) {
if (!userRepository.existsById(id)) {
throw new UserNotFoundException(id);
}
userRepository.deleteById(id);
}
public List<UserDto> search(String email, int page, int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
Page<User> users = email != null
? userRepository.findByEmailContainingIgnoreCase(email, pageable)
: userRepository.findAll(pageable);
return users.stream()
.map(UserDto::fromEntity)
.collect(Collectors.toList());
}
}
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Derived query methods
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
List<User> findByNameContainingIgnoreCase(String name);
// Paginated queries
Page<User> findByEmailContainingIgnoreCase(String email, Pageable pageable);
// Custom JPQL query
@Query("SELECT u FROM User u WHERE u.createdAt > :date AND u.active = true")
List<User> findActiveUsersCreatedAfter(@Param("date") LocalDateTime date);
// Native SQL query
@Query(value = "SELECT * FROM users WHERE email ILIKE %:email%", nativeQuery = true)
List<User> searchByEmail(@Param("email") String email);
// Modifying query
@Modifying
@Query("UPDATE User u SET u.active = false WHERE u.lastLoginAt < :date")
int deactivateInactiveUsers(@Param("date") LocalDateTime date);
}
@Entity
@Table(name = "users")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String name;
@Column(name = "password_hash", nullable = false)
private String passwordHash;
@Column(nullable = false)
@Builder.Default
private boolean active = true;
@Column(name = "created_at", nullable = false, updatable = false)
@CreationTimestamp
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
@UpdateTimestamp
private LocalDateTime updatedAt;
// Relationships
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private List<Post> posts = new ArrayList<>();
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
@Builder.Default
private Set<Role> roles = new HashSet<>();
}
spring:
application:
name: myapp
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: ${DB_USERNAME:postgres}
password: ${DB_PASSWORD:password}
hikari:
maximum-pool-size: 10
minimum-idle: 5
connection-timeout: 30000
jpa:
hibernate:
ddl-auto: validate # none, validate, update, create, create-drop
show-sql: false
properties:
hibernate:
format_sql: true
default_schema: public
profiles:
active: ${SPRING_PROFILES_ACTIVE:dev}
server:
port: ${PORT:8080}
servlet:
context-path: /api
# Actuator endpoints
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: when_authorized
# Custom properties
app:
jwt:
secret: ${JWT_SECRET:your-secret-key}
expiration-ms: 86400000
# application-dev.yml
spring:
jpa:
show-sql: true
h2:
console:
enabled: true
logging:
level:
com.example.myapp: DEBUG
org.springframework.web: DEBUG
---
# application-prod.yml
spring:
jpa:
show-sql: false
properties:
hibernate:
generate_statistics: false
logging:
level:
com.example.myapp: INFO
org.springframework.web: WARN
@Configuration
@ConfigurationProperties(prefix = "app.jwt")
@Data
public class JwtProperties {
private String secret;
private long expirationMs;
}
// Usage
@Service
@RequiredArgsConstructor
public class JwtService {
private final JwtProperties jwtProperties;
public String generateToken(User user) {
return Jwts.builder()
.setSubject(user.getEmail())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtProperties.getExpirationMs()))
.signWith(Keys.hmacShaKeyFor(jwtProperties.getSecret().getBytes()))
.compact();
}
}
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// Handle validation errors
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationErrors(MethodArgumentNotValidException ex) {
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.toList());
ErrorResponse response = ErrorResponse.builder()
.status(HttpStatus.BAD_REQUEST.value())
.message("Validation failed")
.errors(errors)
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.badRequest().body(response);
}
// Handle not found exceptions
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
ErrorResponse response = ErrorResponse.builder()
.status(HttpStatus.NOT_FOUND.value())
.message(ex.getMessage())
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
}
// Handle business logic exceptions
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException ex) {
ErrorResponse response = ErrorResponse.builder()
.status(HttpStatus.CONFLICT.value())
.message(ex.getMessage())
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.status(HttpStatus.CONFLICT).body(response);
}
// Catch-all handler
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleAllExceptions(Exception ex) {
log.error("Unexpected error occurred", ex);
ErrorResponse response = ErrorResponse.builder()
.status(HttpStatus.INTERNAL_SERVER_ERROR.value())
.message("An unexpected error occurred")
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
}
// Error response DTO
@Data
@Builder
public class ErrorResponse {
private int status;
private String message;
private List<String> errors;
private LocalDateTime timestamp;
}
// Custom exceptions
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String resource, Long id) {
super(String.format("%s not found with id: %d", resource, id));
}
}
public class UserNotFoundException extends ResourceNotFoundException {
public UserNotFoundException(Long id) {
super("User", id);
}
}
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final UserDetailsService userDetailsService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers("/actuator/health").permitAll()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
throws Exception {
return config.getAuthenticationManager();
}
}
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
final String jwt = authHeader.substring(7);
final String userEmail = jwtService.extractUsername(jwt);
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(userEmail);
if (jwtService.isTokenValid(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
# Built-in endpoints
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus,env
base-path: /actuator
endpoint:
health:
show-details: when_authorized
probes:
enabled: true # Kubernetes liveness/readiness probes
info:
env:
enabled: true
# Application info
info:
app:
name: ${spring.application.name}
version: '@project.version@'
java:
version: ${java.version}
Common Actuator Endpoints:
GET /actuator/health - Application healthGET /actuator/health/liveness - Kubernetes liveness probeGET /actuator/health/readiness - Kubernetes readiness probeGET /actuator/info - Application informationGET /actuator/metrics - Metrics listGET /actuator/metrics/{name} - Specific metricGET /actuator/prometheus - Prometheus format metrics@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Autowired
private ObjectMapper objectMapper;
@Test
void shouldReturnUserById() throws Exception {
UserDto user = UserDto.builder()
.id(1L)
.email("[email protected]")
.name("Test User")
.build();
when(userService.findById(1L)).thenReturn(Optional.of(user));
mockMvc.perform(get("/api/v1/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.email").value("[email protected]"));
}
@Test
void shouldReturn404WhenUserNotFound() throws Exception {
when(userService.findById(999L)).thenReturn(Optional.empty());
mockMvc.perform(get("/api/v1/users/999"))
.andExpect(status().isNotFound());
}
@Test
void shouldCreateUser() throws Exception {
CreateUserRequest request = new CreateUserRequest();
request.setEmail("[email protected]");
request.setName("New User");
request.setPassword("password123");
UserDto created = UserDto.builder()
.id(1L)
.email("[email protected]")
.name("New User")
.build();
when(userService.create(any())).thenReturn(created);
mockMvc.perform(post("/api/v1/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.email").value("[email protected]"));
}
}
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
@Transactional
class UserIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private UserRepository userRepository;
@Test
void shouldCreateAndRetrieveUser() {
CreateUserRequest request = new CreateUserRequest();
request.setEmail("[email protected]");
request.setName("Integration Test");
request.setPassword("password123");
ResponseEntity<UserDto> createResponse = restTemplate.postForEntity(
"/api/v1/users", request, UserDto.class);
assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(createResponse.getBody()).isNotNull();
assertThat(createResponse.getBody().getEmail()).isEqualTo("[email protected]");
Long userId = createResponse.getBody().getId();
ResponseEntity<UserDto> getResponse = restTemplate.getForEntity(
"/api/v1/users/" + userId, UserDto.class);
assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(getResponse.getBody().getName()).isEqualTo("Integration Test");
}
}
@DataJpaTest
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Autowired
private TestEntityManager entityManager;
@Test
void shouldFindByEmail() {
User user = User.builder()
.email("[email protected]")
.name("Test")
.passwordHash("hash")
.build();
entityManager.persistAndFlush(user);
Optional<User> found = userRepository.findByEmail("[email protected]");
assertThat(found).isPresent();
assertThat(found.get().getName()).isEqualTo("Test");
}
}
// Prefer constructor injection with final fields
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository; // final = immutable
}
// Controller -> Service -> Repository
// DTOs for API layer, Entities for persistence layer
// Never expose entities directly in REST responses
@Service
@Transactional(readOnly = true) // Default read-only
public class UserService {
@Transactional // Write transaction
public void updateUser() { }
}
# Use environment variables for secrets
spring:
datasource:
password: ${DB_PASSWORD} # From environment
// Use @RestControllerAdvice for global exception handling
// Return consistent error responses
// Never expose internal details in production
Beyond framework patterns, watch for a recurring set of robustness and changeability defects in services, controllers, and exception handlers. These are caught by static analysis but are easy to introduce by hand:
catch (Exception) buried in a method — catch the specific types you can
handle. A broad catch is correct only at the top-level boundary (e.g. the
@RestControllerAdvice catch-all), commented as intentional.RuntimeException/Exception — throw typed domain exceptions so
@ExceptionHandler can map them to the right HTTP status.try/catch — extract the inner concern into its own method.if … else if with no final else and switch with no default — handle the
residual case or document why none is needed (defensive programming).switch (Java 14+) over enums/sealed types for compiler-enforced exhaustiveness.if and loop-counter modification inside the loop body —
combine conditions; never reassign a for counter in its body.See Quality Anti-Patterns for each defect with compliant/non-compliant Java examples, severities, and false-positive filters. Patterns derived from CAST Highlight code quality indicators (https://doc.casthighlight.com/).
When using Spring Boot, consider these complementary skills:
tools
Xquik X data automation API - Use REST or MCP for tweet search, user lookup, follower exports, media downloads, monitors, webhooks, giveaway draws, and confirmation-gated X actions.
tools
LinkedIn automation via the Linked API CLI - fetch profiles, search people and companies, send messages, manage connections, create posts, react, comment, and run Sales Navigator and custom workflows. Use when the user wants to interact with LinkedIn.
tools
MCP (Model Context Protocol) server build and evaluation guide, including local conventions for tool surfaces, config, and testing
tools
MCP (Model Context Protocol) - Build AI-native servers with tools, resources, and prompts. TypeScript/Python SDKs for Claude Desktop integration.