Spring Boot ToDoリストAPIを作る|4層構造・JPA・CRUDをマスター

Spring Boot ToDoリストAPIを作る|4層構造・JPA・CRUDをマスター

この記事はJava学習シリーズ「課題4」です。まだ環境が整っていない方は Java環境構築ガイド を先に読んでください。課題1〜3(パスワードチェッカーデータ分析API為替変換API)でSpring Bootの基本を学んでいることを前提とします。


この記事で作るもの

Spring Bootで動くタスク管理REST APIです。フロントエンドは不要で、curlコマンドやInsomniaでAPIを叩いてテストします。

# タスクを作成
POST /api/tasks
{"title": "Javaを学ぶ", "priority": "HIGH"}
→ 201 Created

# 全タスクを取得
GET /api/tasks
→ [{"id":1, "title":"Javaを学ぶ", ...}]

# 完了にする
PATCH /api/tasks/1/complete
→ 200 OK

# 削除
DELETE /api/tasks/1
→ 204 No Content

この記事で学べること

課題1〜3では @RestController@Service を使ってAPIを作りました。今回は実務でよく使われる**4層構造(Controller / Service / Repository / Entity)**を導入し、データベースとの連携まで実装します。

  • 4層構造(Controller / Service / Repository / Entity の役割分担)
  • Spring Data JPA(インターフェースを書くだけでDB操作が使える)
  • @Entity(JavaクラスをDBのテーブルとして扱う仕組み)
  • DIコンテナ(依存関係の自動注入の仕組み)
  • HTTPステータスコード(200 / 201 / 204 / 404 / 400)

HTTPステータスコードとは? APIのレスポンスに付いてくる「結果の番号」です。ブラウザのエラーページで見る「404 Not Found」がその一例です。

コード意味
200 OK成功
201 Created新規作成成功
204 No Content成功(返すデータなし)
400 Bad Requestリクエストの形式が間違っている
404 Not Found指定したリソースが存在しない
500 Internal Server Errorサーバー側のバグ

1. 要件定義:何を作るかを整理する

コードを書く前に「このAPIが何をしなければならないか」を整理します。今回はCRUD(作成・読み取り・更新・削除)の操作を備えたToDoリストAPIです。

#エンドポイントやること理由
1POST /api/tasksタスクを新規作成するユーザーがタスクを登録できるようにする
2GET /api/tasks全タスクを取得するタスクの一覧を表示するため
3GET /api/tasks/{id}IDで1件取得する特定タスクの詳細を見るため
4PATCH /api/tasks/{id}/completeタスクを完了にする完了フラグを更新するビジネスロジックが必要
5DELETE /api/tasks/{id}タスクを削除する不要なタスクを削除できるようにする
6バリデーションタイトルが空のリクエストを弾く不正なデータがDBに入らないようにする

2. Spring Initializrでプロジェクトを生成する

Spring BootのプロジェクトはSpring Initializr(start.spring.io)で雛形を生成するのが標準的な手順です。

start.spring.io の設定

Project:       Maven
Language:      Java
Spring Boot:   3.3.x(最新安定版)

Group:         com.example
Artifact:      todo-api
Name:          todo-api

Java:          21

Dependencies(右側の「ADD DEPENDENCIES」をクリックして追加):
  ✓ Spring Web
  ✓ Spring Data JPA
  ✓ H2 Database
  ✓ Validation
  ✓ Lombok

「GENERATE」ボタンでZIPファイルをダウンロード。解凍してIntelliJで開きます(「File → Open」でフォルダを選択)。

生成されたプロジェクト構造

todo-api/
├── pom.xml
└── src/main/java/com/example/todoapi/
    └── TodoApiApplication.java    ← Spring Bootの起動クラス

3. プロジェクト構造を理解する

課題4〜5で使う4層構造(Layer Architecture)を最初に理解します。

リクエスト → Controller → Service → Repository → DB
            └→ Entity(データの形を定義)
レイヤークラス役割
ControllerTaskControllerHTTPリクエストを受け取り、レスポンスを返す
ServiceTaskServiceビジネスロジック(完了フラグの設定など)
RepositoryTaskRepositoryDBへの読み書き(JPA)
EntityTaskDBのテーブルと1対1対応するデータクラス

4. Entityクラスを作る

src/main/java/com/example/todoapi/ 配下に entity パッケージを作り、Task.java を作成します。

IntelliJでフォルダを右クリック → New → Package → entity 次に entity を右クリック → New → Java Class → Task

package com.example.todoapi.entity;

import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;

@Entity                   // このクラスはDBのテーブルに対応する、という宣言
@Table(name = "tasks")    // テーブル名を指定
@Data                     // Lombok: getter/setter/toString/equals/hashCodeを自動生成
@NoArgsConstructor        // Lombok: 引数なしのコンストラクタを自動生成
public class Task {

    @Id                                             // 主キー
    @GeneratedValue(strategy = GenerationType.IDENTITY)  // AUTO_INCREMENT
    private Long id;

    @Column(nullable = false, length = 100)  // NOT NULL, 最大100文字
    private String title;

    @Column(length = 500)
    private String description;

    @Column(nullable = false)
    private boolean completed = false;

    @Enumerated(EnumType.STRING)  // 列挙型をSTRINGとして保存("LOW"/"MEDIUM"/"HIGH")
    @Column(nullable = false)
    private Priority priority = Priority.MEDIUM;

    @Column(updatable = false)  // 作成後は変更しない
    private LocalDateTime createdAt = LocalDateTime.now();

    public enum Priority {
        LOW, MEDIUM, HIGH
    }
}

アノテーションとは

アノテーション(Annotation)とは? クラスやメソッドに付ける「マーカー(ラベル)」のようなものです。@Entity@Column のように @ で始まります。Javaコンパイラやフレームワーク(Spring等)がこれを読み取って自動的に処理を追加してくれます。「このクラスはDBのテーブルに対応している」「このメソッドはGETリクエストを処理する」といった情報を付加するものと理解してください。

Lombokとは

Lombok(ロンボク)とは? Javaの「ボイラープレートコード(繰り返し書かないといけない定型コード)」を自動生成するライブラリです。@Data を付けるだけで、全フィールドのgetter/setter/toString/equals/hashCodeが自動生成されます。クラウドワークスの既存コードにも頻繁に登場します。

@Data @NoArgsConstructor はLombokのアノテーションです。本来なら何十行もかかる getter/setter/toString を自動生成してくれます。

// Lombokなし: 手で全部書く
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
// ... (全フィールド分)

// Lombokあり: @Data を付けるだけ
@Data
public class Task { ... }  // getter/setter は全部自動生成される

5. Repositoryを作る

package com.example.todoapi.repository;

import com.example.todoapi.entity.Task;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;

// JpaRepository<Task, Long> を継承するだけでCRUDが全部使える!
public interface TaskRepository extends JpaRepository<Task, Long> {

    // メソッド名のルールに従って書くだけでSQLが自動生成される(魔法)
    List<Task> findByCompleted(boolean completed);
    List<Task> findByPriority(Task.Priority priority);
    List<Task> findByTitleContainingIgnoreCase(String keyword);
}

JpaRepository を継承するだけで以下のメソッドが使えます:

メソッド動作
findAll()全件取得
findById(id)IDで1件取得
save(task)保存・更新
deleteById(id)IDで削除
count()件数取得

6. DTOを作る(リクエスト / レスポンスの形)

Entityクラスができましたが、このEntityをそのままAPIのレスポンスとして返すのは問題があります。なぜなら、DBの内部構造(カラム名・リレーション・Javaのアノテーション情報)がそのまま外部に露出してしまうからです。

そこで**DTO(Data Transfer Object)**を間に挟みます。DTOはAPIの「入口と出口の形」だけを定義したクラスです。

DTOとは? 「このAPIにはこの形でリクエストを送ってください」「このAPIはこの形でデータを返します」というデータの形(形式)を定義するクラスです。内部実装(DB構造)を隠して、APIの利用者に必要な情報だけを渡せます。たとえば Task EntityにパスワードやDB管理用のカラムがあっても、DTOに含めなければ外部に漏れません。

dto パッケージを作り、3つのファイルを作成します。

TaskResponse.java(レスポンス形式):

package com.example.todoapi.dto;

import com.example.todoapi.entity.Task;
import java.time.LocalDateTime;

public record TaskResponse(
    Long id,
    String title,
    String description,
    boolean completed,
    String priority,
    LocalDateTime createdAt
) {
    // Taskエンティティから変換するファクトリメソッド
    public static TaskResponse from(Task task) {
        return new TaskResponse(
            task.getId(),
            task.getTitle(),
            task.getDescription(),
            task.isCompleted(),
            task.getPriority().name(),
            task.getCreatedAt()
        );
    }
}

CreateTaskRequest.java(タスク作成リクエスト):

package com.example.todoapi.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import com.example.todoapi.entity.Task;

public record CreateTaskRequest(
    @NotBlank(message = "タイトルは必須です")
    @Size(max = 100, message = "タイトルは100文字以内です")
    String title,

    String description,

    Task.Priority priority  // null の場合はService側でデフォルト値を設定
) {}

record はJava 16で追加された機能で、イミュータブル(変更不可)なデータクラスを簡潔に書けます。


7. Serviceクラスを作る

package com.example.todoapi.service;

import com.example.todoapi.dto.CreateTaskRequest;
import com.example.todoapi.dto.TaskResponse;
import com.example.todoapi.entity.Task;
import com.example.todoapi.repository.TaskRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;

@Service            // このクラスはServiceである、という宣言(DIコンテナに登録される)
@RequiredArgsConstructor  // Lombok: finalフィールドのコンストラクタを自動生成(DIに必要)
public class TaskService {

    // @RequiredArgsConstructorによってコンストラクタインジェクションが自動設定される
    // → Spring が TaskRepository のインスタンスを自動で作って注入してくれる
    private final TaskRepository taskRepository;

    // 全タスク取得
    public List<TaskResponse> findAll() {
        return taskRepository.findAll().stream()
            .map(TaskResponse::from)
            .toList();
    }

    // ID指定で1件取得
    public Optional<TaskResponse> findById(Long id) {
        return taskRepository.findById(id)
            .map(TaskResponse::from);
    }

    // 完了状態でフィルタリング
    public List<TaskResponse> findByCompleted(boolean completed) {
        return taskRepository.findByCompleted(completed).stream()
            .map(TaskResponse::from)
            .toList();
    }

    // タスク作成
    public TaskResponse create(CreateTaskRequest req) {
        Task task = new Task();
        task.setTitle(req.title());
        task.setDescription(req.description());
        task.setPriority(req.priority() != null ? req.priority() : Task.Priority.MEDIUM);
        Task saved = taskRepository.save(task);
        return TaskResponse.from(saved);
    }

    // タスク完了にする
    public Optional<TaskResponse> complete(Long id) {
        return taskRepository.findById(id).map(task -> {
            task.setCompleted(true);
            return TaskResponse.from(taskRepository.save(task));
        });
    }

    // タスク削除
    public void delete(Long id) {
        taskRepository.deleteById(id);
    }
}

DIとは何か

DI(Dependency Injection・依存性の注入)とは? @Service@Controller が付いたクラスは、Spring が自動的にインスタンスを作って管理します。これを「DIコンテナ」と呼びます。TaskServiceTaskRepository を使いたいとき、自分で new TaskRepository() する必要がなく、Spring が自動で渡してくれます。

// DIなし: 使いたいクラスを自分で new する必要がある
TaskRepository repository = new TaskRepository();

// Spring DI: Springが自動でインスタンスを作って注入してくれる
@RequiredArgsConstructor
public class TaskService {
    private final TaskRepository taskRepository; // 宣言するだけで自動で注入される
}

8. Controllerを作る

package com.example.todoapi.controller;

import com.example.todoapi.dto.CreateTaskRequest;
import com.example.todoapi.dto.TaskResponse;
import com.example.todoapi.service.TaskService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController          // JSON形式でレスポンスを返す
@RequestMapping("/api/tasks")  // このControllerは /api/tasks 配下を担当
@RequiredArgsConstructor
public class TaskController {

    private final TaskService taskService;

    // GET /api/tasks または GET /api/tasks?completed=true
    @GetMapping
    public List<TaskResponse> getAllTasks(
            @RequestParam(required = false) Boolean completed) {
        if (completed != null) {
            return taskService.findByCompleted(completed);
        }
        return taskService.findAll();
    }

    // GET /api/tasks/1
    @GetMapping("/{id}")
    public ResponseEntity<TaskResponse> getTask(@PathVariable Long id) {
        return taskService.findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    // POST /api/tasks
    @PostMapping
    public ResponseEntity<TaskResponse> createTask(
            @Valid @RequestBody CreateTaskRequest req) {
        TaskResponse created = taskService.create(req);
        return ResponseEntity.status(HttpStatus.CREATED).body(created);
    }

    // PATCH /api/tasks/1/complete
    @PatchMapping("/{id}/complete")
    public ResponseEntity<TaskResponse> completeTask(@PathVariable Long id) {
        return taskService.complete(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    // DELETE /api/tasks/1
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteTask(@PathVariable Long id) {
        taskService.delete(id);
        return ResponseEntity.noContent().build();
    }
}

9. H2データベースの設定

src/main/resources/application.properties に設定を追加します。

# H2インメモリDBの設定(開発・テスト用)
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver

# JPA設定
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true

# H2コンソールを有効化(ブラウザでDBの中を見られる)
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

10. 起動してAPIをテストする

TodoApiApplication.javamain メソッド横の緑の三角▶をクリックして起動します。

コンソールに以下が表示されれば成功:

Tomcat started on port 8080 (http) with context path ''
Started TodoApiApplication in 2.345 seconds

curlでAPIをテスト

# タスク作成
curl -X POST http://localhost:8080/api/tasks \
  -H "Content-Type: application/json" \
  -d '{"title": "Javaを学ぶ", "priority": "HIGH"}'

# 全件取得
curl http://localhost:8080/api/tasks

# 完了にする
curl -X PATCH http://localhost:8080/api/tasks/1/complete

# 完了済みのみ取得
curl "http://localhost:8080/api/tasks?completed=true"

# 削除
curl -X DELETE http://localhost:8080/api/tasks/1

バリデーションのテスト

# タイトルなしで作成 → 400 Bad Requestが返る
curl -X POST http://localhost:8080/api/tasks \
  -H "Content-Type: application/json" \
  -d '{"description": "タイトルがない"}'

H2コンソールでDBを確認

ブラウザで http://localhost:8080/h2-console を開き、以下を入力してConnect:

JDBC URL: jdbc:h2:mem:testdb
User:     sa
Password: (空欄)

SELECT * FROM TASKS; を実行すると、作成したタスクがテーブルで確認できます。


まとめ:学んだこと

概念内容
Spring Initializrプロジェクトの雛形生成
4層構造Controller / Service / Repository / Entity
@RestControllerJSONを返すControllerの宣言
@GetMapping / @PostMappingエンドポイントの定義
ResponseEntityステータスコード付きレスポンス
JpaRepositoryCRUDメソッドの自動実装
@Valid / @NotBlankリクエストバリデーション
DIコンテナ@Service / @RequiredArgsConstructor
recordイミュータブルなデータクラス

Javaの学習シリーズ: 環境構築 | 課題1・パスワードチェッカー | 課題2・CSV集計ツール | 課題3・為替APIクライアント | 今ここ(課題4・Spring Boot Todo API) | 課題5・家計簿API

この記事をシェアする

関連記事


CHECK IT OUT

Members Only

サブスク加入者限定の コンテンツを配信中

  • 毎月のAI・自動化トレンドレポート
  • クリエイター向け業務効率化テンプレート
  • ツール選定・SaaS比較まとめ(限定公開)
Discord サーバー & LINE 公式の両方で通知します
クリエイター・個人事業主向け 受付中

「これ自動化できないかな?」 そのアイデア、ITで実現できます。

業務の自動化・お客様向けツール開発・AI活用まで、クリエイター・個人事業主専門のITコンサルタントが対応します。まずは気軽にご相談ください。

LINEで相談 フォームで相談