C
Spring Boot/Web MVC/Lesson 06

REST API CRUD 実践 — 最初から最後まで

1時間·theory
このチャプター
2/2

REST API CRUD 実践 — 最初から最後まで

🎯 このレッスンを読み終えたら

このレッスンを読み終えると、以下の3つを自信を持って実践できるようになります。

  • ✅ @RestController + @RequestMapping を使って REST API 4種 (GET/POST/PUT/DELETE) を実装する
  • ✅ @RequestBody / @PathVariable / @RequestParam の違いと使い方を理解する
  • ✅ @RestControllerAdvice でグローバル例外処理パターンを適用する

学習目標をチェックリストとして手元に置き、すべてに答えられるようになったらレッスンを閉じてください。

CRUD の4つの HTTP メソッド

REST の基本的な約束

操作HTTP メソッドURL 例レスポンス
一覧取得GET/users200 + 配列
単件取得GET/users/1200 + オブジェクト / 404
作成POST/users201 + 作成されたオブジェクト
更新PUT/users/1200 + 更新されたオブジェクト
削除DELETE/users/1204 (No Content)

URL は名詞(リソース)、操作は HTTP メソッド — これが REST の本質です。

1. User ドメイン + DTO

java
@Getter @Setter
@NoArgsConstructor @AllArgsConstructor
public class User {
    private Long id;
    private String email;
    private String name;
}

@Getter @Setter
public class UserCreateRequest {
    private String email;
    private String name;
}

Entity と DTO を分離するのが標準的なアプローチです。DTO は API の入出力専用、Entity は データベースマッピング専用 です。

2. 仮のストレージ(データベースの代わりにインメモリ)

java
@Repository
public class UserRepository {
    private final Map<Long, User> store = new ConcurrentHashMap<>();
    private final AtomicLong seq = new AtomicLong();

    public List<User> findAll()            { return new ArrayList<>(store.values()); }
    public Optional<User> findById(Long id) { return Optional.ofNullable(store.get(id)); }
    public User save(User u) {
        if (u.getId() == null) u.setId(seq.incrementAndGet());
        store.put(u.getId(), u);
        return u;
    }
    public void deleteById(Long id) { store.remove(id); }
}

3. サービスレイヤー

java
@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository repo;

    public List<User> list() { return repo.findAll(); }

    public User get(Long id) {
        return repo.findById(id)
            .orElseThrow(() -> new NotFoundException("User not found: " + id));
    }

    public User create(UserCreateRequest req) {
        return repo.save(new User(null, req.getEmail(), req.getName()));
    }

    public User update(Long id, UserCreateRequest req) {
        User u = get(id);
        u.setEmail(req.getEmail());
        u.setName(req.getName());
        return repo.save(u);
    }

    public void delete(Long id) { repo.deleteById(id); }
}

Controller が Repository を直接操作することはありません — ビジネスロジックは Service レイヤーが担います。

Controller — @RequestBody · @PathVariable · ResponseEntity

4つのアノテーションの役割

  • @RequestBody — HTTP ボディの JSON をオブジェクトへデシリアライズする
  • @PathVariable — URL パスの {id} のような値を取り出す
  • @RequestParam?page=1 のようなクエリ文字列の値を取り出す
  • ResponseEntityステータスコード + ヘッダー + ボディを直接制御する

コントローラーの全コード

java
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {
    private final UserService service;

    // GET /users
    @GetMapping
    public List<User> list() {
        return service.list();
    }

    // GET /users/{id}
    @GetMapping("/{id}")
    public User get(@PathVariable Long id) {
        return service.get(id);
    }

    // POST /users
    @PostMapping
    public ResponseEntity<User> create(@RequestBody UserCreateRequest req) {
        User created = service.create(req);
        return ResponseEntity
            .status(HttpStatus.CREATED)            // 201
            .body(created);
    }

    // PUT /users/{id}
    @PutMapping("/{id}")
    public User update(@PathVariable Long id, @RequestBody UserCreateRequest req) {
        return service.update(id, req);
    }

    // DELETE /users/{id}
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        service.delete(id);
        return ResponseEntity.noContent().build();    // 204
    }
}

レスポンスボディがオブジェクトの場合 — Jackson が自動でシリアライズ

@RestController が付いていると、すべての戻り値が自動的に JSON へ変換されます。MapList・DTO をそのまま返すだけで、追加設定は不要です。

ステータスコードを明示したい場合

GET はオブジェクトをそのまま返す、POST は 201、DELETE は 204 — これを明示するために ResponseEntity を使います。

実務では、すべてのレスポンスを ResponseEntity で統一するチームと、シンプルな GET はオブジェクトだけ返すチームに分かれます。どちらも正解です。

グローバル例外処理 — @ControllerAdvice

いたるところに try-catch は地獄

java
// ❌ コントローラーごとに例外処理
@GetMapping("/{id}")
public ResponseEntity<?> get(@PathVariable Long id) {
    try {
        return ResponseEntity.ok(service.get(id));
    } catch (NotFoundException e) {
        return ResponseEntity.status(404).body(e.getMessage());
    }
}

数十のメソッドに try-catch → ボイラープレートが爆発。

@ControllerAdvice例外処理専用 Bean

java
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(NotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(NotFoundException e) {
        return ResponseEntity
            .status(HttpStatus.NOT_FOUND)
            .body(new ErrorResponse("NOT_FOUND", e.getMessage()));
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ErrorResponse> handleBadInput(IllegalArgumentException e) {
        return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(new ErrorResponse("BAD_REQUEST", e.getMessage()));
    }

    @ExceptionHandler(Exception.class)   // *最後の砦*
    public ResponseEntity<ErrorResponse> handleAll(Exception e) {
        log.error("Unhandled exception", e);
        return ResponseEntity
            .status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(new ErrorResponse("INTERNAL_ERROR", "サーバーエラー"));
    }
}

@Getter @AllArgsConstructor
class ErrorResponse {
    private String code;
    private String message;
}

すべてのコントローラーの例外を一か所で処理します。 コントローラーのコードはビジネスロジックだけに集中できるようになります。

カスタム例外クラス

java
public class NotFoundException extends RuntimeException {
    public NotFoundException(String msg) { super(msg); }
}

RuntimeException を継承することで、throws 宣言が不要になります。ドメインごと (UserNotFoundException・OrderNotFoundException) に作るとコードの意図が明確になります。

curl テスト

bash
# 1. 作成
curl -X POST http://localhost:8080/users \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","name":"Alice"}'
# → 201 {"id":1,"email":"[email protected]","name":"Alice"}

# 2. 取得
curl http://localhost:8080/users/1
# → 200 {"id":1,...}

# 3. 存在しない ID の取得
curl -i http://localhost:8080/users/999
# → 404 {"code":"NOT_FOUND","message":"User not found: 999"}

# 4. 削除
curl -X DELETE http://localhost:8080/users/1 -i
# → 204 No Content

Postman ユーザーは4つのリクエストを Collection として保存しておけば、ワンクリックでテストできます。

🤖 AI にはこう依頼してみてください

このレッスンの概念を理解すれば、AI に具体的かつ的確な指示が出せるようになります。漠然とした「直して」ではなく、語彙を持ったリクエスト — それがトークン節約の出発点です。

  • 「このコントローラーに @ControllerAdvice ベースのグローバル例外処理を追加して」
  • 「User CRUD API 4種 (GET/POST/PUT/DELETE) のコントローラーを作って」
  • 「このレスポンスを ResponseEntity でラップして、201 / 204 ステータスコードを正確に返すようにして」

なぜこれがトークンを減らすのか

概念を知らないと、AI の回答を受け取っても「それって何ですか?」と再度聞き直す羽目になります。その「聞き直し」がトークンを消費します。概念を一度しっかり覚えれば、会話が一度で完結します。

REST API CRUD 実践 — 最初から最後まで - Spring Boot