この記事は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です。
| # | エンドポイント | やること | 理由 |
|---|---|---|---|
| 1 | POST /api/tasks | タスクを新規作成する | ユーザーがタスクを登録できるようにする |
| 2 | GET /api/tasks | 全タスクを取得する | タスクの一覧を表示するため |
| 3 | GET /api/tasks/{id} | IDで1件取得する | 特定タスクの詳細を見るため |
| 4 | PATCH /api/tasks/{id}/complete | タスクを完了にする | 完了フラグを更新するビジネスロジックが必要 |
| 5 | DELETE /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(データの形を定義)
| レイヤー | クラス | 役割 |
|---|---|---|
| Controller | TaskController | HTTPリクエストを受け取り、レスポンスを返す |
| Service | TaskService | ビジネスロジック(完了フラグの設定など) |
| Repository | TaskRepository | DBへの読み書き(JPA) |
| Entity | Task | DBのテーブルと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の利用者に必要な情報だけを渡せます。たとえば
TaskEntityにパスワードや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コンテナ」と呼びます。TaskServiceがTaskRepositoryを使いたいとき、自分で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.java の main メソッド横の緑の三角▶をクリックして起動します。
コンソールに以下が表示されれば成功:
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 |
@RestController | JSONを返すControllerの宣言 |
@GetMapping / @PostMapping | エンドポイントの定義 |
ResponseEntity | ステータスコード付きレスポンス |
JpaRepository | CRUDメソッドの自動実装 |
@Valid / @NotBlank | リクエストバリデーション |
| DIコンテナ | @Service / @RequiredArgsConstructor |
record | イミュータブルなデータクラス |
Javaの学習シリーズ: 環境構築 | 課題1・パスワードチェッカー | 課題2・CSV集計ツール | 課題3・為替APIクライアント | 今ここ(課題4・Spring Boot Todo API) | 課題5・家計簿API
関連記事
CHECK IT OUT
サブスク加入者限定の
コンテンツを配信中
- 毎月のAI・自動化トレンドレポート
- クリエイター向け業務効率化テンプレート
- ツール選定・SaaS比較まとめ(限定公開)
「これ自動化できないかな?」
そのアイデア、ITで実現できます。
業務の自動化・お客様向けツール開発・AI活用まで、クリエイター・個人事業主専門のITコンサルタントが対応します。まずは気軽にご相談ください。