mcpField
S
spring-boot-patterns

spring-boot-patterns

github.com/decebals開発761334 回インストール7 週間前に更新
リポジトリ

spring-boot-patterns について

Spring Boot 開発者向けのパターンガイドです。アプリケーション構成、実装の指針、テストを扱い、新規プロジェクトや機能開発、コードレビューに役立ちます。

主な機能

  1. 01プロジェクト構成の指針
  2. 02REST コントローラーの実践例
  3. 03サービスとリポジトリーのパターン
  4. 04DTO マッピングと例外処理
  5. 05設定とテストの例

活用シーン

  1. 01Spring Boot のプロジェクト構成を作る
  2. 02REST API とビジネスロジックを実装する
  3. 03データアクセスと例外処理を確認する
  4. 04コントローラーテストや統合テストを書く

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/decebals/claude-code-java --skill spring-boot-patterns

インストール後、Claude Code で/spring-boot-patternsを入力して読み込まれたか確認します。

同梱ファイルREADME.md

詳細

Spring Boot パターンのスキル

Spring Boot アプリケーションのベストプラクティスとパターン。

使用する場面

  • ユーザーが「コントローラーを作成して」/「サービスを追加して」/「Spring Boot のサポート」と言った場合
  • Spring Boot コードをレビューする場合
  • 新しい Spring Boot プロジェクトの構成を設定する場合

プロジェクト構成

src/main/java/com/example/myapp/
├── MyAppApplication.java          # @SpringBootApplication
├── config/                        # Configuration classes
│   ├── SecurityConfig.java
│   └── WebConfig.java
├── controller/                    # REST controllers
│   └── UserController.java
├── service/                       # Business logic
│   ├── UserService.java
│   └── impl/
│       └── UserServiceImpl.java
├── repository/                    # Data access
│   └── UserRepository.java
├── model/                         # Entities
│   └── User.java
├── dto/                           # Data transfer objects
│   ├── request/
│   │   └── CreateUserRequest.java
│   └── response/
│       └── UserResponse.java
├── exception/                     # Custom exceptions
│   ├── ResourceNotFoundException.java
│   └── GlobalExceptionHandler.java
└── util/                          # Utilities
    └── DateUtils.java

コントローラーパターン

REST コントローラーのテンプレート
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor  // Lombok for constructor injection
public class UserController {

    private final UserService userService;

    @GetMapping
    public ResponseEntity<List<UserResponse>> getAll() {
        return ResponseEntity.ok(userService.findAll());
    }

    @GetMapping("/{id}")
    public ResponseEntity<UserResponse> getById(@PathVariable Long id) {
        return ResponseEntity.ok(userService.findById(id));
    }

    @PostMapping
    public ResponseEntity<UserResponse> create(
            @Valid @RequestBody CreateUserRequest request) {
        UserResponse created = userService.create(request);
        URI location = ServletUriComponentsBuilder.fromCurrentRequest()
            .path("/{id}")
            .buildAndExpand(created.getId())
            .toUri();
        return ResponseEntity.created(location).body(created);
    }

    @PutMapping("/{id}")
    public ResponseEntity<UserResponse> update(
            @PathVariable Long id,
            @Valid @RequestBody UpdateUserRequest request) {
        return ResponseEntity.ok(userService.update(id, request));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        userService.delete(id);
        return ResponseEntity.noContent().build();
    }
}
コントローラーのベストプラクティス
プラクティス例
バージョン付き API/api/v1/users
複数形の名詞/user ではなく /users
HTTP メソッドGET=読み取り、POST=作成、PUT=更新、DELETE=削除
ステータスコード200=OK、201=Created、204=NoContent、404=NotFound
バリデーションリクエストボディに @Valid
❌ アンチパターン
// ❌ Business logic in controller
@PostMapping
public User create(@RequestBody User user) {
    user.setCreatedAt(LocalDateTime.now());  // Logic belongs in service
    return userRepository.save(user);         // Direct repo access
}

// ❌ Returning entity directly (exposes internals)
@GetMapping("/{id}")
public User getById(@PathVariable Long id) {
    return userRepository.findById(id).get();
}

サービスパターン

サービスインターフェース+実装
// Interface
public interface UserService {
    List<UserResponse> findAll();
    UserResponse findById(Long id);
    UserResponse create(CreateUserRequest request);
    UserResponse update(Long id, UpdateUserRequest request);
    void delete(Long id);
}

// Implementation
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)  // Default read-only
public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;
    private final UserMapper userMapper;

    @Override
    public List<UserResponse> findAll() {
        return userRepository.findAll().stream()
            .map(userMapper::toResponse)
            .toList();
    }

    @Override
    public UserResponse findById(Long id) {
        return userRepository.findById(id)
            .map(userMapper::toResponse)
            .orElseThrow(() -> new ResourceNotFoundException("User", id));
    }

    @Override
    @Transactional  // Write transaction
    public UserResponse create(CreateUserRequest request) {
        User user = userMapper.toEntity(request);
        User saved = userRepository.save(user);
        return userMapper.toResponse(saved);
    }

    @Override
    @Transactional
    public void delete(Long id) {
        if (!userRepository.existsById(id)) {
            throw new ResourceNotFoundException("User", id);
        }
        userRepository.deleteById(id);
    }
}
サービスのベストプラクティス
  • テスト容易性のため、インターフェース+実装クラスを使用する
  • クラスレベルで @Transactional(readOnly = true) を指定する
  • 書き込みメソッドには @Transactional を指定する
  • 汎用的な例外ではなく、ドメイン例外をスローする
  • エンティティと DTO の変換にはマッパー(MapStruct)を使用する

リポジトリパターン

JPA リポジトリ
public interface UserRepository extends JpaRepository<User, Long> {

    // Derived query
    Optional<User> findByEmail(String email);

    List<User> findByActiveTrue();

    // Custom query
    @Query("SELECT u FROM User u WHERE u.department.id = :deptId")
    List<User> findByDepartmentId(@Param("deptId") Long departmentId);

    // Native query (use sparingly)
    @Query(value = "SELECT * FROM users WHERE created_at > :date",
           nativeQuery = true)
    List<User> findRecentUsers(@Param("date") LocalDate date);

    // Exists check (more efficient than findBy)
    boolean existsByEmail(String email);

    // Count
    long countByActiveTrue();
}
リポジトリのベストプラクティス
  • 可能な場合は導出クエリを使用する
  • 単一の結果には Optional を使用する
  • 存在確認には findBy ではなく existsBy を使用する
  • 必要な場合を除き、ネイティブクエリを避ける
  • フェッチを最適化するには @EntityGraph を使用する

DTO パターン

リクエスト/レスポンス DTO
// Request DTO with validation
public record CreateUserRequest(
    @NotBlank(message = "Name is required")
    @Size(min = 2, max = 100)
    String name,

    @NotBlank
    @Email(message = "Invalid email format")
    String email,

    @NotNull
    @Min(18)
    Integer age
) {}

// Response DTO
public record UserResponse(
    Long id,
    String name,
    String email,
    LocalDateTime createdAt
) {}
MapStruct マッパー
@Mapper(componentModel = "spring")
public interface UserMapper {

    UserResponse toResponse(User entity);

    List<UserResponse> toResponseList(List<User> entities);

    @Mapping(target = "id", ignore = true)
    @Mapping(target = "createdAt", ignore = true)
    User toEntity(CreateUserRequest request);
}

例外処理

カスタム例外
public class ResourceNotFoundException extends RuntimeException {

    public ResourceNotFoundException(String resource, Long id) {
        super(String.format("%s not found with id: %d", resource, id));
    }
}

public class BusinessException extends RuntimeException {

    private final String code;

    public BusinessException(String code, String message) {
        super(message);
        this.code = code;
    }
}
グローバル例外ハンドラー
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
        log.warn("Resource not found: {}", ex.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
            .body(new ErrorResponse("NOT_FOUND", ex.getMessage()));
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(
            MethodArgumentNotValidException ex) {
        List<String> errors = ex.getBindingResult().getFieldErrors().stream()
            .map(e -> e.getField() + ": " + e.getDefaultMessage())
            .toList();
        return ResponseEntity.badRequest()
            .body(new ErrorResponse("VALIDATION_ERROR", errors.toString()));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneric(Exception ex) {
        log.error("Unexpected error", ex);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(new ErrorResponse("INTERNAL_ERROR", "An unexpected error occurred"));
    }
}

public record ErrorResponse(String code, String message) {}

設定パターン

アプリケーションプロパティ
# application.yml
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: ${DB_USER}
    password: ${DB_PASSWORD}
  jpa:
    hibernate:
      ddl-auto: validate  # Never 'create' in production!
    show-sql: false

app:
  jwt:
    secret: ${JWT_SECRET}
    expiration: 86400000
設定プロパティクラス
@Configuration
@ConfigurationProperties(prefix = "app.jwt")
@Validated
public class JwtProperties {

    @NotBlank
    private String secret;

    @Min(60000)
    private long expiration;

    // getters and setters
}
プロファイル固有の設定
src/main/resources/
├── application.yml           # Common config
├── application-dev.yml       # Development
├── application-test.yml      # Testing
└── application-prod.yml      # Production

よく使うアノテーションのクイックリファレンス

アノテーション用途
@RestControllerREST コントローラー(@Controller と @ResponseBody を組み合わせたもの)
@Serviceビジネスロジックコンポーネント
@Repositoryデータアクセスコンポーネント
@Configuration設定クラス
@RequiredArgsConstructorLombok:コンストラクターインジェクション
@Transactionalトランザクション管理
@Validバリデーションを実行
@ConfigurationPropertiesプロパティをクラスにバインド
@Profile("dev")プロファイル固有の Bean
@Scheduledスケジュールされたタスク

テストパターン

コントローラーテスト(MockMvc)
@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Test
    void shouldReturnUser() throws Exception {
        when(userService.findById(1L))
            .thenReturn(new UserResponse(1L, "John", "[email protected]", null));

        mockMvc.perform(get("/api/v1/users/1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("John"));
    }
}
サービステスト
@ExtendWith(MockitoExtension.class)
class UserServiceImplTest {

    @Mock
    private UserRepository userRepository;

    @Mock
    private UserMapper userMapper;

    @InjectMocks
    private UserServiceImpl userService;

    @Test
    void shouldThrowWhenUserNotFound() {
        when(userRepository.findById(1L)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> userService.findById(1L))
            .isInstanceOf(ResourceNotFoundException.class);
    }
}
統合テスト
@SpringBootTest
@AutoConfigureMockMvc
@Testcontainers
class UserIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");

    @Autowired
    private MockMvc mockMvc;

    @Test
    void shouldCreateUser() throws Exception {
        mockMvc.perform(post("/api/v1/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                    {"name": "John", "email": "[email protected]", "age": 25}
                    """))
            .andExpect(status().isCreated());
    }
}

クイックリファレンスカード

レイヤー責務アノテーション
コントローラーHTTP の処理、バリデーション@RestController、@Valid
サービスビジネスロジック、トランザクション@Service、@Transactional
リポジトリデータアクセス@Repository、JpaRepository を拡張
DTOデータ転送バリデーションアノテーション付きのレコード
設定設定@Configuration、@ConfigurationProperties
例外エラー処理@RestControllerAdvice

コメント