Spring Boot + JavaScript: 非同期でフォームを動的に更新する実装方法メモ

Spring Bootの\*\*`@RestController`**と、モダンなJavaScriptの**Fetch API**および**`async/await`\*\*構文を組み合わせ、この非同期更新処理をクリーンかつ再利用性の高い形で実装する方法をまとめる。

## 1\. システム全体の構成

実現したい処理の全体像は以下の通り。バックエンドとフロントエンドの役割を明確に分離するのが目的。

  * **Backend (Spring Boot)**

    1.  画面のフォームに対応したデータ転送オブジェクト（DTO / Formクラス）を定義する。
    2.  `@RestController`を用いて、このDTOをJSON形式で返すAPIエンドポイントを作成する。

  * **Frontend (HTML / JavaScript)**

    1.  HTMLで入力フォームと、処理のきっかけとなるボタンを配置する。各入力項目には、バックエンドのDTOのフィールド名と一致する`id`属性を付与する。
    2.  ボタンクリックなどのイベントをトリガーに、JavaScriptでバックエンドのAPIを呼び出す。
    3.  APIから受け取ったJSONデータを解析し、各入力項目の`value`に値を設定して画面を更新する。

*(簡易的な構成イメージ)*

## 2\. Backend: Spring Bootの実装

まず、JSONデータを返すAPIをSpring Bootで実装する。
前提として、Spring Bootのプロジェクトがセットアップされている必要がある。

### 2.1. Formオブジェクト（DTO）の作成

APIが返すJSONの構造を定義するJavaクラス（POJO）を作成する。

`com/example/demo/form/UserForm.java`

```java
package com.example.demo.form;

public class UserForm {
    private String name;
    private int age;
    private String email;

    // Getters and Setters
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
}
```

### 2.3. RestControllerの作成

`@RestController`アノテーションを使い、オブジェクトを返すと自動的にJSONに変換されるAPIコントローラを実装する。

`com/example/demo/controller/UserApiController.java`

```java
package com.example.demo.controller;

import com.example.demo.form.UserForm;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api") // このコントローラのパスの接頭辞を/apiとする
public class UserApiController {

    @GetMapping("/user") // GETリクエストで /api/user にアクセスされた際に呼び出される
    public UserForm getUserData() {
        // 本来はDBアクセスなどの処理を行う
        UserForm form = new UserForm();
        form.setName("山田 太郎");
        form.setAge(30);
        form.setEmail("taro.yamada@example.com");
        return form;
    }
}
```

### 2.4. 画面表示用Controllerの作成

ThymeleafでレンダリングするHTMLページを表示するための、通常の`@Controller`も用意する。

`com/example/demo/controller/PageController.java`

```java
package com.example.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class PageController {

    @GetMapping("/user/form")
    public String showUserForm() {
        return "user-form"; // templates/user-form.html を返す
    }
}
```

## 3\. Frontend: JavaScriptとHTMLの実装

次に、バックエンドAPIと通信するフロントエンド部分を実装する。

### 3.1. HTMLの準備

各入力項目（`<input>`）の`id`属性値が、`UserForm`クラスのフィールド名（`name`, `age`, `email`）と正確に一致するようにHTMLを作成する。これが、JavaScriptから要素を特定するための重要なルールである。

`templates/user-form.html`

```html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>ユーザー情報フォーム</title>
</head>
<body>
    <h1>ユーザー情報</h1>
    <form id="userForm">
        <div>
            <label for="name">氏名:</label>
            <input type="text" id="name" name="name">
        </div>
        <div>
            <label for="age">年齢:</label>
            <input type="number" id="age" name="age">
        </div>
        <div>
            <label for="email">メールアドレス:</label>
            <input type="email" id="email" name="email">
        </div>
    </form>
    <button type="button" id="loadDataBtn">データ読み込み</button>

    <script>
        // ...
    </script>
</body>
</html>
```

### 3.2. JavaScriptによる非同期処理の実装

API通信とDOM操作のロジックをJavaScriptで記述する。ここでは、再利用性を考慮して処理を共通関数化し、`async/await`構文を用いて呼び出すモダンなアプローチを採用する。

#### **Step 1: 共通関数の作成**

まず、APIからデータを取得してフォームに値を設定する責務を持つ、汎用的な関数を作成する。この関数は、処理の結果を扱うために`Promise`を返す設計とする。

```javascript
/**
 * APIからデータを非同期で取得し、JSONのキーと一致するIDを持つフォーム要素に値を設定する。
 * この関数は処理の成功/失敗を扱うためのPromiseを返す。
 *
 * @param {string} apiUrl - データ取得先のAPIのURL
 * @returns {Promise<object>} 成功時にはAPIから取得したデータオブジェクトで解決(resolve)され、
 * 失敗時にはエラーで拒否(reject)されるPromise。
 */
function fetchDataAndBindToForm(apiUrl) {
    return new Promise((resolve, reject) => {
        fetch(apiUrl)
            .then(response => {
                if (!response.ok) {
                    reject(new Error(`Network response was not ok. Status: ${response.status}`));
                    return;
                }
                return response.json();
            })
            .then(data => {
                if (!data) {
                    reject(new Error('Received empty data from API.'));
                    return;
                }
                Object.keys(data).forEach(key => {
                    const element = document.getElementById(key);
                    if (element) {
                        element.value = data[key];
                    } else {
                        console.warn(`Warning: Element with ID '${key}' not found.`);
                    }
                });
                resolve(data);
            })
            .catch(error => {
                console.error('Fetch operation failed:', error);
                reject(error);
            });
    });
}
```

#### **Step 2: async/awaitによる関数の呼び出し**

次に、ボタンのクリックイベントを捕捉し、作成した共通関数を`async/await`構文を使って呼び出す。

**`async/await`とは？**
`async`と`await`は、JavaScriptの非同期処理を、まるで同期処理（上から順に実行されるコード）のように直感的に記述するための仕組み。
`.then()`をチェーンする従来の書き方に比べて、コードの可読性が飛躍的に向上し、エラーハンドリングも`try...catch`で簡潔に行える。

HTMLの`<script>`タグ内に、以下のコードを追記する。

```javascript
// ... fetchDataAndBindToForm関数の定義 ...

// HTMLのDOM構造が読み込まれた後に実行する
document.addEventListener('DOMContentLoaded', () => {
    const loadButton = document.getElementById('loadDataBtn');

    // ボタンのクリックイベントにasync関数を登録
    loadButton.addEventListener('click', async () => {
        try {
            // awaitで非同期処理の完了を待つ
            const data = await fetchDataAndBindToForm('/api/user');
            
            // 成功した場合の追加処理
            console.log('データが正常にバインドされました:', data);
            alert('データを読み込みました！');

        } catch (error) {
            // 失敗した場合のエラーハンドリング
            console.error('処理に失敗しました:', error);
            alert('データの読み込みに失敗しました。');
        }
    });
});
```