backend-java/springboot-project-starter/SKILL.md
Scaffold a production-ready Spring Boot 3.3+ application with Java 21+, Spring Data JPA, Spring Security, Flyway migrations, and comprehensive testing.
npx skillsauth add achreftlili/deep-dev-skills springboot-project-starterInstall 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.
Scaffold a production-ready Spring Boot 3.3+ application with Java 21+, Spring Data JPA, Spring Security, Flyway migrations, and comprehensive testing.
# Via Spring Initializr CLI (curl)
curl -s https://start.spring.io/starter.zip \
-d type=maven-project \
-d language=java \
-d bootVersion=3.3.5 \
-d javaVersion=21 \
-d groupId=com.example \
-d artifactId=myapp \
-d name=myapp \
-d packageName=com.example.myapp \
-d dependencies=web,data-jpa,security,validation,flyway,postgresql,testcontainers,lombok \
-o myapp.zip && unzip myapp.zip -d myapp
# Via Spring Boot CLI (if installed)
spring init --boot-version=3.3.5 --java-version=21 \
--dependencies=web,data-jpa,security,validation,flyway,postgresql,testcontainers,lombok \
--groupId=com.example --artifactId=myapp --name=myapp myapp.zip
myapp/
├── src/
│ ├── main/
│ │ ├── java/com/example/myapp/
│ │ │ ├── MyappApplication.java
│ │ │ ├── config/
│ │ │ │ ├── SecurityConfig.java
│ │ │ │ └── WebConfig.java
│ │ │ ├── controller/
│ │ │ │ └── UserController.java
│ │ │ ├── dto/
│ │ │ │ ├── UserRequest.java
│ │ │ │ └── UserResponse.java
│ │ │ ├── entity/
│ │ │ │ └── User.java
│ │ │ ├── exception/
│ │ │ │ ├── GlobalExceptionHandler.java
│ │ │ │ └── ResourceNotFoundException.java
│ │ │ ├── mapper/
│ │ │ │ └── UserMapper.java
│ │ │ ├── repository/
│ │ │ │ └── UserRepository.java
│ │ │ └── service/
│ │ │ └── UserService.java
│ │ └── resources/
│ │ ├── application.yml
│ │ ├── application-dev.yml
│ │ ├── application-prod.yml
│ │ └── db/migration/
│ │ └── V1__create_users_table.sql
│ └── test/
│ └── java/com/example/myapp/
│ ├── MyappApplicationTests.java
│ ├── controller/
│ │ └── UserControllerTest.java
│ └── service/
│ └── UserServiceTest.java
├── pom.xml
└── docker-compose.yml
UserRequest, UserResponse).@Entity. Never expose entities directly in controllers.@Mapper(componentModel = "spring").@Autowired on fields.@Valid, @NotBlank, @Size, etc.).V{version}__{description}.sql.dev for local development (H2 or Docker Postgres), prod for production.ResponseEntity<T> for explicit HTTP status control.@WebMvcTest for controllers, @DataJpaTest for repositories, @SpringBootTest for integration.package com.example.myapp.entity;
import jakarta.persistence.*;
import java.time.Instant;
@Entity
@Table(name = "users")
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(nullable = false, updatable = false)
private Instant createdAt;
private Instant updatedAt;
@PrePersist
void onCreate() {
this.createdAt = Instant.now();
}
@PreUpdate
void onUpdate() {
this.updatedAt = Instant.now();
}
// Constructors
protected User() {}
public User(String email, String name) {
this.email = email;
this.name = name;
}
// Getters and setters
public Long getId() { return id; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Instant getCreatedAt() { return createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
}
package com.example.myapp.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public record UserRequest(
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100)
String name,
@NotBlank(message = "Email is required")
@Email(message = "Email must be valid")
String email
) {}
package com.example.myapp.dto;
import java.time.Instant;
public record UserResponse(
Long id,
String name,
String email,
Instant createdAt
) {}
package com.example.myapp.mapper;
import com.example.myapp.dto.UserRequest;
import com.example.myapp.dto.UserResponse;
import com.example.myapp.entity.User;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
@Mapper(componentModel = "spring")
public interface UserMapper {
User toEntity(UserRequest request);
UserResponse toResponse(User user);
void updateEntity(UserRequest request, @MappingTarget User user);
}
package com.example.myapp.repository;
import com.example.myapp.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
}
package com.example.myapp.service;
import com.example.myapp.dto.UserRequest;
import com.example.myapp.dto.UserResponse;
import com.example.myapp.entity.User;
import com.example.myapp.exception.ResourceNotFoundException;
import com.example.myapp.mapper.UserMapper;
import com.example.myapp.repository.UserRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class UserService {
private final UserRepository userRepository;
private final UserMapper userMapper;
public UserService(UserRepository userRepository, UserMapper userMapper) {
this.userRepository = userRepository;
this.userMapper = userMapper;
}
@Transactional(readOnly = true)
public List<UserResponse> findAll() {
return userRepository.findAll()
.stream()
.map(userMapper::toResponse)
.toList();
}
@Transactional(readOnly = true)
public UserResponse findById(Long id) {
return userRepository.findById(id)
.map(userMapper::toResponse)
.orElseThrow(() -> new ResourceNotFoundException("User", id));
}
@Transactional
public UserResponse create(UserRequest request) {
User user = userMapper.toEntity(request);
return userMapper.toResponse(userRepository.save(user));
}
@Transactional
public UserResponse update(Long id, UserRequest request) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User", id));
userMapper.updateEntity(request, user);
return userMapper.toResponse(userRepository.save(user));
}
@Transactional
public void delete(Long id) {
if (!userRepository.existsById(id)) {
throw new ResourceNotFoundException("User", id);
}
userRepository.deleteById(id);
}
}
package com.example.myapp.controller;
import com.example.myapp.dto.UserRequest;
import com.example.myapp.dto.UserResponse;
import com.example.myapp.service.UserService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public ResponseEntity<List<UserResponse>> findAll() {
return ResponseEntity.ok(userService.findAll());
}
@GetMapping("/{id}")
public ResponseEntity<UserResponse> findById(@PathVariable Long id) {
return ResponseEntity.ok(userService.findById(id));
}
@PostMapping
public ResponseEntity<UserResponse> create(@Valid @RequestBody UserRequest request) {
return ResponseEntity.status(HttpStatus.CREATED).body(userService.create(request));
}
@PutMapping("/{id}")
public ResponseEntity<UserResponse> update(
@PathVariable Long id,
@Valid @RequestBody UserRequest request) {
return ResponseEntity.ok(userService.update(id, request));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.noContent().build();
}
}
package com.example.myapp.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers("/actuator/health").permitAll()
.anyRequest().authenticated()
)
.build();
}
}
package com.example.myapp.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.Map;
import java.util.stream.Collectors;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ProblemDetail handleNotFound(ResourceNotFoundException ex) {
return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
Map<String, String> errors = ex.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(
fe -> fe.getField(),
fe -> fe.getDefaultMessage() != null ? fe.getDefaultMessage() : "Invalid",
(a, b) -> a
));
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.BAD_REQUEST, "Validation failed");
problem.setProperty("errors", errors);
return problem;
}
}
-- V1__create_users_table.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ
);
spring:
application:
name: myapp
profiles:
active: dev
jpa:
open-in-view: false
hibernate:
ddl-auto: validate
properties:
hibernate:
format_sql: true
flyway:
enabled: true
locations: classpath:db/migration
server:
port: 8080
shutdown: graceful
management:
endpoints:
web:
exposure:
include: health,info,metrics
spring:
datasource:
url: jdbc:postgresql://localhost:5432/myapp_dev
username: postgres
password: postgres
jpa:
properties:
hibernate:
format_sql: true
logging:
level:
com.example.myapp: DEBUG
org.springframework.security: DEBUG
org.hibernate.SQL: DEBUG
org.hibernate.orm.jdbc.bind: TRACE
spring:
datasource:
url: ${DATABASE_URL}
username: ${DATABASE_USERNAME}
password: ${DATABASE_PASSWORD}
logging:
level:
com.example.myapp: INFO
org.hibernate.SQL: WARN
package com.example.myapp.service;
import com.example.myapp.dto.UserRequest;
import com.example.myapp.dto.UserResponse;
import com.example.myapp.entity.User;
import com.example.myapp.exception.ResourceNotFoundException;
import com.example.myapp.mapper.UserMapper;
import com.example.myapp.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 java.time.Instant;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock UserRepository userRepository;
@Mock UserMapper userMapper;
@InjectMocks UserService userService;
@Test
void findById_existingUser_returnsResponse() {
User user = new User("[email protected]", "Alice");
UserResponse response = new UserResponse(1L, "Alice", "[email protected]", Instant.now());
when(userRepository.findById(1L)).thenReturn(Optional.of(user));
when(userMapper.toResponse(user)).thenReturn(response);
UserResponse result = userService.findById(1L);
assertThat(result.name()).isEqualTo("Alice");
verify(userRepository).findById(1L);
}
@Test
void findById_missingUser_throwsNotFound() {
when(userRepository.findById(99L)).thenReturn(Optional.empty());
assertThatThrownBy(() -> userService.findById(99L))
.isInstanceOf(ResourceNotFoundException.class);
}
@Test
void create_validRequest_savesAndReturns() {
UserRequest request = new UserRequest("Bob", "[email protected]");
User entity = new User("[email protected]", "Bob");
UserResponse response = new UserResponse(2L, "Bob", "[email protected]", Instant.now());
when(userMapper.toEntity(request)).thenReturn(entity);
when(userRepository.save(entity)).thenReturn(entity);
when(userMapper.toResponse(entity)).thenReturn(response);
UserResponse result = userService.create(request);
assertThat(result.email()).isEqualTo("[email protected]");
verify(userRepository).save(any(User.class));
}
}
package com.example.myapp.controller;
import com.example.myapp.dto.UserResponse;
import com.example.myapp.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.bean.MockBean;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import java.time.Instant;
import java.util.List;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired MockMvc mockMvc;
@MockBean UserService userService;
@Test
@WithMockUser
void findAll_returnsOkWithUsers() throws Exception {
when(userService.findAll()).thenReturn(List.of(
new UserResponse(1L, "Alice", "[email protected]", Instant.now())
));
mockMvc.perform(get("/api/v1/users"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].name").value("Alice"));
}
}
createdb myapp_devsrc/main/resources/application-dev.yml with your database credentials./mvnw compile to generate MapStruct sources and verify the project compiles./mvnw spring-boot:run -Dspring-boot.run.profiles=dev (Flyway runs migrations automatically on startup)curl http://localhost:8080/actuator/health# Run the application (dev profile)
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev
# Run all tests
./mvnw test
# Run a specific test class
./mvnw test -Dtest=UserServiceTest
# Package as JAR
./mvnw clean package -DskipTests
# Run the packaged JAR
java -jar target/myapp-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod
# Build Docker image (with Spring Boot's built-in Buildpack support)
./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=myapp:latest
# Check for dependency updates
./mvnw versions:display-dependency-updates
# Generate MapStruct sources (happens automatically during compile)
./mvnw compile
WebMvcConfigurer bean or @CrossOrigin on controllers when pairing with a frontend skill (React, Angular, etc.).springdoc-openapi-starter-webmvc-ui dependency for auto-generated Swagger UI at /swagger-ui.html.micrometer-registry-prometheus for Prometheus metrics and spring-boot-starter-actuator (already included) for health checks.spring-boot:build-image goal produces an OCI image without a Dockerfile. For custom Dockerfiles, use a multi-stage build with Eclipse Temurin as the base../mvnw wrapper ensures reproducible builds without requiring Maven on the CI server.testing
Set up Vitest 2.x with TypeScript for unit and component testing using test/describe/it, vi.fn/vi.mock/vi.spyOn, component testing with Testing Library, coverage (v8/istanbul), workspace config, and snapshot testing.
testing
Set up pytest 8.x with Python for unit and integration testing using fixtures (scope, autouse, parametrize), async tests (pytest-asyncio), mocking (unittest.mock, pytest-mock), coverage (pytest-cov), conftest.py patterns, and markers.
testing
Set up Playwright 1.49+ with TypeScript for E2E testing using page object model, fixtures, test.describe/test blocks, assertions, selectors, network mocking, CI configuration, and trace viewer.
testing
Set up Jest 30+ with TypeScript for unit tests, integration tests, mocking (jest.fn, jest.mock, jest.spyOn), coverage configuration, custom matchers, snapshot testing, and setup/teardown patterns.