Java×SpringBootでTODOアプリのRestAPIを作る

## はじめに

本記事では、JavaとSpring Bootを使用して、シンプルなTODOアプリのREST APIを作成する方法を紹介します。タスクの作成、更新、削除、取得ができるAPIを実装します。このAPIを作成することで、Spring Bootの基本的な使い方を学び、実際のプロジェクトでよく使われるRESTful APIの設計についても理解を深めることができます。

### 使用する技術
- **Java 21**
- **Spring Boot 3.4.2**
- **Gradle**
- **H2データベース**（軽量で開発用のインメモリデータベース）
- **Lombok**

## プロジェクトのセットアップ

### 1. Spring Initializrを使ったプロジェクトの作成

Spring Initializrを使って、基本的なSpring Bootプロジェクトを作成します。

1. **Spring Initializrのサイトにアクセス**  
   [https://start.spring.io/](https://start.spring.io/) にアクセスします。

2. **プロジェクトメタデータを設定**
   - **Project**: Gradle Project
   - **Language**: Java
   - **Spring Boot**: 3.4.2
   - **Project Metadata**:
     - **Group**: com.example
     - **Artifact**: todo-api
     - **Name**: todo-api
     - **Description**: A simple Todo API
     - **Package name**: com.example.todo
     - **Packaging**: Jar
     - **Java**: 21

3. **依存関係の選択**  
   以下の依存関係を追加します。
   - **Spring Web** (REST API作成に必要)
   - **Spring Data JPA** (JPAを使ってデータベース操作)
   - **H2 Database** (開発用のインメモリデータベース)
   - **Lombok** (ボイラープレートコードを減らすため)

4. **プロジェクトの生成**  
   上記の設定を終えたら、「Generate」ボタンをクリックしてプロジェクトをダウンロードします。

5. **ダウンロードしたファイルを解凍して、IDE（IntelliJやEclipseなど）で開きます**。

### 2. プロジェクトの依存関係設定（`build.gradle`）

ダウンロードしたプロジェクトには、`build.gradle`ファイルが自動で生成されています。内容は以下のようになっていますが、念のため確認してください。

#### `build.gradle`

```gradle
plugins {
    id 'org.springframework.boot' version '3.4.2'
    id 'io.spring.dependency-management' version '1.1.0'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '21'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'com.h2database:h2'
    implementation 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
    useJUnitPlatform()
}
```

## 3. `application.properties`の設定

次に、`src/main/resources/application.properties`に、H2データベースの設定を追加します。

#### `src/main/resources/application.properties`

```properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
```

## 4. エンティティの作成

次に、TODOタスクを表す`Todo`エンティティクラスを作成します。
```
package com.example.todo.entity;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Data
@Entity
public class Todo {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;
    private String description;
    private boolean completed;
}
```

## 5. リポジトリの作成

`Todo`エンティティを操作するためのリポジトリを作成します。

```
package com.example.todo.repository;

import com.example.todo.entity.Todo;
import org.springframework.data.jpa.repository.JpaRepository;

public interface TodoRepository extends JpaRepository<Todo, Long> {
}
```

## 6. サービス層の作成

サービス層は、ビジネスロジックを担当します。TODOタスクの作成、更新、削除、取得を担当するメソッドを実装します。

```
package com.example.todo.service;

import com.example.todo.entity.Todo;
import com.example.todo.repository.TodoRepository;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
public class TodoService {

    private final TodoRepository todoRepository;

    public TodoService(TodoRepository todoRepository) {
        this.todoRepository = todoRepository;
    }

    public List<Todo> getAllTodos() {
        return todoRepository.findAll();
    }

    public Optional<Todo> getTodoById(Long id) {
        return todoRepository.findById(id);
    }

    public Todo createTodo(Todo todo) {
        return todoRepository.save(todo);
    }

    public Todo updateTodo(Long id, Todo todo) {
        if (!todoRepository.existsById(id)) {
            return null;
        }
        todo.setId(id);
        return todoRepository.save(todo);
    }

    public boolean deleteTodo(Long id) {
        if (todoRepository.existsById(id)) {
            todoRepository.deleteById(id);
            return true;
        }
        return false;
    }
}
```

## 7. コントローラーの作成

次に、REST APIを提供するコントローラーを作成します。
```
package com.example.todo.controller;

import com.example.todo.entity.Todo;
import com.example.todo.service.TodoService;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/api/todos")
public class TodoController {

    private final TodoService todoService;

    public TodoController(TodoService todoService) {
        this.todoService = todoService;
    }

    @GetMapping
    public List<Todo> getAllTodos() {
        return todoService.getAllTodos();
    }

    @GetMapping("/{id}")
    public Optional<Todo> getTodoById(@PathVariable Long id) {
        return todoService.getTodoById(id);
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Todo createTodo(@RequestBody Todo todo) {
        return todoService.createTodo(todo);
    }

    @PutMapping("/{id}")
    public Todo updateTodo(@PathVariable Long id, @RequestBody Todo todo) {
        return todoService.updateTodo(id, todo);
    }

    @DeleteMapping("/{id}")
    public void deleteTodo(@PathVariable Long id) {
        if (!todoService.deleteTodo(id)) {
            throw new RuntimeException("Todo not found");
        }
    }
}
```

## 8. 動作確認

Spring Bootアプリケーションが正常に動作することを確認するために、以下の手順で確認します。

### 8.1 H2コンソールの使用

Spring BootはH2データベースをインメモリで使用しており、H2コンソールを利用してデータベースの内容を確認できます。

1. `application.properties`ファイルに以下の設定があることを確認して、H2コンソールを有効にしていることを確認します。
    ```properties
    spring.h2.console.enabled=true
    spring.h2.console.path=/h2-console
    ```

2. アプリケーションを起動します。

3. ブラウザで `http://localhost:8080/h2-console` にアクセスします。

4. データベースのURLを確認します。`jdbc:h2:mem:testdb` と設定されていれば、そのままでOKです。usernameとpasswordは、application.propertiesに書いている値を記載します。

5. 「Connect」ボタンをクリックして、H2コンソールに接続します。これで、TODOテーブルの内容が確認できるようになります。

### 8.2 PostmanでAPIを確認

次に、Postmanやcurlなどを使用して、REST APIの動作を確認します。

- **GET /api/todos**: すべてのTODOを取得
- **GET /api/todos/{id}**: 指定したIDのTODOを取得
- **POST /api/todos**: 新しいTODOを作成
- **PUT /api/todos/{id}**: 指定したIDのTODOを更新
- **DELETE /api/todos/{id}**: 指定したIDのTODOを削除

各エンドポイントにリクエストを送信して、正常に動作することを確認します。

---

## 9. テストの作成

アプリケーションの動作をテストするために、`MockMvc`を使ってREST APIのテストを行います。`MockMvc`は、Spring MVCのテスト用に提供されているツールで、実際にサーバーを立ち上げることなく、HTTPリクエストをシミュレーションできます。

### 9.1 テストクラスの作成

以下のコードで、TODOアプリケーションのREST APIに対するテストを作成します。

```java
package com.example.todo;

import com.example.todo.entity.Todo;
import com.example.todo.repository.TodoRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest
@AutoConfigureMockMvc
public class TodoControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private TodoRepository todoRepository;

    @Autowired
    private ObjectMapper objectMapper;

    @BeforeEach
    public void setUp() {
        todoRepository.deleteAll();
    }

    @Test
    public void testGetAllTodos() throws Exception {
        Todo todo1 = new Todo();
        todo1.setTitle("Test Todo 1");
        todo1.setDescription("Description for test todo 1");
        todo1.setCompleted(false);
        todoRepository.save(todo1);

        mockMvc.perform(get("/api/todos"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$[0].title").value("Test Todo 1"))
                .andExpect(jsonPath("$[0].description").value("Description for test todo 1"));
    }

    @Test
    public void testCreateTodo() throws Exception {
        Todo todo = new Todo();
        todo.setTitle("New Todo");
        todo.setDescription("This is a new todo");
        todo.setCompleted(false);

        mockMvc.perform(post("/api/todos")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(todo)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.title").value("New Todo"))
                .andExpect(jsonPath("$.description").value("This is a new todo"));

        // DBに新しいTODOが作成されたか確認
        Todo createdTodo = todoRepository.findAll().get(0);
        assertEquals("New Todo", createdTodo.getTitle());
        assertEquals("This is a new todo", createdTodo.getDescription());
    }

    @Test
    public void testUpdateTodo() throws Exception {
        Todo todo = new Todo();
        todo.setTitle("Todo to update");
        todo.setDescription("Description of todo");
        todo.setCompleted(false);
        Todo savedTodo = todoRepository.save(todo);

        savedTodo.setTitle("Updated Todo");
        savedTodo.setDescription("Updated description");

        mockMvc.perform(put("/api/todos/" + savedTodo.getId())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(savedTodo)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.title").value("Updated Todo"))
                .andExpect(jsonPath("$.description").value("Updated description"));

        // DBの値が更新されたことを確認
        Todo updatedTodo = todoRepository.findById(savedTodo.getId()).get();
        assertEquals("Updated Todo", updatedTodo.getTitle());
        assertEquals("Updated description", updatedTodo.getDescription());
    }

    @Test
    public void testDeleteTodo() throws Exception {
        Todo todo = new Todo();
        todo.setTitle("Todo to delete");
        todo.setDescription("Description of todo to delete");
        todo.setCompleted(false);
        Todo savedTodo = todoRepository.save(todo);

        mockMvc.perform(delete("/api/todos/" + savedTodo.getId()))
                .andExpect(status().isOk());

        // DBから削除されていることを確認
        assertTrue(todoRepository.findById(savedTodo.getId()).isEmpty());
    }
}

```

### 9.2 テストの説明

- **testGetAllTodos**: `GET /api/todos`エンドポイントをテストします。保存したTODOが返されることを確認します。
- **testCreateTodo**: `POST /api/todos`エンドポイントをテストします。新しいTODOを作成し、レスポンスが正しいことを確認した後、DBに新しいTODOが保存されたことも検証します。
- **testUpdateTodo**: `PUT /api/todos/{id}`エンドポイントをテストします。既存のTODOを更新し、レスポンスに反映されることを確認した後、DBに保存されたTODOの内容が更新されていることも検証します。
- **testDeleteTodo**: `DELETE /api/todos/{id}`エンドポイントをテストします。指定したIDのTODOを削除し、そのTODOがDBから削除されたことを確認します。

### 9.3 テスト実行方法

依存関係にJUnitを使用している場合、IDEで直接テストを実行するか、以下のコマンドでテストを実行できます。

```bash
./gradlew test
```

## 結論

この記事では、Java 21とSpring Boot 3.4.2を使用して、シンプルなTODOアプリのREST APIを実装しました。これで、Spring Bootを使ったRESTful APIの基本的な実装方法を理解できたと思います。

## 参考文献

- [Spring Initializr](https://start.spring.io/)
- [Spring Boot公式ドキュメント](https://spring.io/projects/spring-boot)
- [H2 Database公式サイト](https://www.h2database.com/html/main.html)
- [Lombok公式サイト](https://projectlombok.org/)