コンテンツにスキップ

10-5. fetchでフロントエンドとAPIを接続する

  • fetchasync / await で REST API を呼び出す基本
  • GET / POST / PATCH / DELETE の使い分け
  • 読み込み中表示・送信中表示・失敗時のエラー表示を実装する方法
  • response.ok / status を見て通信エラーを扱う考え方
  • Network タブでリクエスト・レスポンスを追い、自力で原因を絞る方法

10-3 では、ブラウザ内の state を更新しながら画面を安定して動かせるようにした。
10-4 では、同じ書籍データを Spring Boot の REST API として扱えるようにした。

このセクションでは、そのフロントエンドを REST API につなぎ、サーバーとやり取りしながら動く書籍管理アプリ へ発展させる。


作業前: HTTP サーバーが起動していることを確認する

Section titled “作業前: HTTP サーバーが起動していることを確認する”

10-1 でフロントエンドの HTTP サーバーを起動した。 このセクションでは fetch()http://localhost:8080 の API を呼び出すため、引き続き book-frontend/ を HTTP サーバー経由で開いている状態 にしておく。

ブラウザで http://localhost:4173/ が開けることを確認してから次へ進む。

ターミナルを閉じるなどしてサーバーが止まっている場合は、book-app/book-frontend/ で次を再実行する。

Terminal window
npx --yes serve . --listen 4173

このセクションの終わりでは、次の動きができる状態を目指す。

  • 画面読み込み時に API から書籍一覧を取得できる
  • フォーム送信で POST し、新しい本を追加できる
  • 状態変更を PATCH でサーバーへ反映できる
  • 削除を DELETE で実行できる
  • 通信中はローディング表示を出し、失敗時はエラー理由を画面へ表示できる
  • Network タブで「どの通信が、どの結果になったか」を追える
ブラウザ UI
↓ fetch
REST API (/api/books)
サーバー側の保存処理
レスポンス(JSON)
state.books を更新
render()

10-5 は、script.js を保存 → ブラウザを更新 → Network と Console で何が起きたか確認する という流れで進めると理解しやすい。

このセクションでは、各実装段階の末尾に「ブラウザを更新して確認する」を置く。毎回、次の順で進めよう。

  1. 指定された HTML / CSS / JavaScript を追加・修正する
  2. 保存する
  3. ブラウザを更新する
  4. 画面・Console・Network のどこに変化が出るかを確認する

途中までは、見た目がまだほとんど変わらない段階もある。
その場合は「通信の土台だけ先に入った」「自動初期化はまだつないでいない」と説明できればよい。


1. なぜブラウザ内 state から API へ進むのか

Section titled “1. なぜブラウザ内 state から API へ進むのか”

ブラウザ内の state だけで動く画面は、UI とロジックの基本を学ぶ段階には向いている。
しかし実務のアプリでは、次のような要求が出てくる。

  • 別の PC やスマホでも同じデータを見たい
  • 複数人で同じデータを共有したい
  • サーバー側で検索や並び替え、認可を行いたい
ブラウザ内 state 版
ブラウザだけで完結
API 連携版
ブラウザ ↔ サーバー ↔ データベース

つまり API 連携は「難しくするため」ではなく、データをブラウザの外へ出して扱うため に必要になる。


2. 今回扱う REST API の形を整理する

Section titled “2. 今回扱う REST API の形を整理する”

ここでは次の API を想定する。

HTTP メソッドエンドポイント役割
GET/api/books一覧取得
POST/api/books新しい本の追加
PATCH/api/books/:id一部の項目だけ更新
DELETE/api/books/:id本を削除

データの例:

{
"id": "b1",
"title": "JavaScript入門",
"author": "山田太郎",
"category": "フロントエンド",
"price": 2800,
"status": "未読",
"memo": "DOM 操作の章まで読む"
}

なぜ HTTP メソッドを分けるのか

Section titled “なぜ HTTP メソッドを分けるのか”

/api/books という同じ名前でも、

  • GET なら読む
  • POST なら作る

というように、操作の意味をメソッドで表現する のが REST の基本である。

PATCH は「項目の一部だけ更新したい」ときに便利で、今回なら状態変更に向いている。


3. まず画面に通信状態の置き場を作る

Section titled “3. まず画面に通信状態の置き場を作る”

前のセクションの HTML に、API 用のメッセージ領域を足す。

3-1. HTML にメッセージ領域を追加する

Section titled “3-1. HTML にメッセージ領域を追加する”

変更する場所: index.html の書籍登録パネル(section.panel)を以下の内容に更新する。赤い行(-)が変更前、緑の行(+)が変更後または新規追加。

index.html
<section class="panel"><!-- すでにある -->
<div class="section-heading">
<h2>書籍を登録する</h2>
<p class="section-note">JavaScript で表示を更新します</p>
<p class="section-note">REST API と連携します</p>
</div>
<p id="form-error" class="form-error" hidden></p>
<div class="message message-error" id="form-errors" hidden></div>
<div class="message message-error" id="api-error" hidden></div>
<div class="message message-info" id="loading-message" hidden></div>
<form class="book-form" id="book-form"><!-- すでにある -->
<!-- 10-3 と同じ入力欄 -->
<button class="primary-button" id="submit-button" type="submit">書籍を追加</button>
</form>
</section>

変更点は2つである。

  • JavaScript で表示を更新します(10-2 で追加)を REST API と連携します へ置き換える
  • 10-3 で追加した #form-error(単数)を削除し、API 連携用の 3 つのメッセージ領域へ差し替える
    • form-errors(複数・入力エラー)、api-error(API 通信エラー)、loading-message(ローディング)
  • ブラウザを更新しても、form-errorsapi-errorloading-messagehidden なのでまだ見えない
  • Elements タブで #form-error が消えて #form-errors#api-error#loading-message がフォームの近くに追加されていることを確認する

この段階では、通信状態を出す置き場だけ先に作った と考えればよい。

通信中メッセージと、あとで使う状態選択 select の見た目も先に整えておく。

追加する場所: style.css の末尾に追加する。

style.css
.message-info {
background: #eff6ff;
border: 1px solid #bfdbfe;
color: #1d4ed8;
}
.status-select {
width: 100%;
min-width: 110px;
padding: 8px 10px;
border: 1px solid var(--line);
border-radius: 10px;
font: inherit;
background: #fff;
}

なぜローディング表示が必要なのか

Section titled “なぜローディング表示が必要なのか”

通信は一瞬で終わるとは限らない。
何も表示しないと、利用者からは「押せていないのか」「待てばよいのか」が分からない。

ボタンを押した
通信中表示がある → いま処理中だと分かる
通信中表示がない → 反応していないように見える
  • ブラウザを更新し、見た目が大きく変わらなくてもよい
  • .message-infoloading-message がまだ hidden のため見えない
  • .status-select も、まだ状態欄を select にしていないので見えなくてよい

この段階では、あとで見える UI の見た目だけ先に仕込んだ と整理すればよい。


4. fetch を共通化する requestJSON() を作る

Section titled “4. fetch を共通化する requestJSON() を作る”

毎回 fetch() の結果確認を書くと長くなるので、共通関数を 1 つ用意する。

追加する場所: script.js の先頭に追加する。

script.js
const API_BASE_URL = 'http://localhost:8080/api/books';
async function requestJSON(url, options = {}) {
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
...(options.headers ?? {}),
},
...options,
});
if (response.status === 204) {
return null;
}
const data = await response.json().catch(() => null);
if (!response.ok) {
const message = data?.message ?? `HTTP ${response.status} エラー`;
throw new Error(message);
}
return data;
}

fetch() は、HTTP 400 や 500 でも「通信自体は返ってきた」と見なして Promise を解決する。
つまり await fetch() だけでは、成功か失敗かを判断しきれない。

通信できた = Promise は解決される
業務的に成功した = response.ok を見ないと分からない

この違いを理解しておくと、「なぜ catch に入らないのに失敗しているのか」で迷いにくい。

  • ブラウザを更新し、Console に構文エラーが出ていないことを確認する
  • まだ画面は変わらなくてよい
  • requestJSON() はまだ他の処理から呼ばれていないので、共通関数の土台だけが入った状態 である

5. ローディングとエラー表示の土台を 1 つずつ作る

Section titled “5. ローディングとエラー表示の土台を 1 つずつ作る”

通信は非同期なので、画面へ状態を出す関数を分けておく。

変更する場所: 既存の state ブロックを削除し、緑の state ブロックへ置き換える。

script.js
const state = {
books: [
{
id: 'b1',
title: 'リーダブルコード',
author: 'Dustin Boswell',
category: '設計',
price: 3200,
status: '未読',
memo: '第1章から読む',
},
],
errorMessage: '',
};
const state = {
books: [],
isLoading: false,
apiErrorMessage: '',
};

変更する場所: 既存の elements ブロックを削除し、緑の elements ブロックへ置き換える。

script.js
const elements = {
form: document.querySelector("#book-form"),
title: document.querySelector("#title"),
author: document.querySelector("#author"),
category: document.querySelector("#category"),
price: document.querySelector("#price"),
status: document.querySelector("#status"),
memo: document.querySelector("#memo"),
error: document.querySelector("#form-error"),
tbody: document.querySelector("#book-table-body"),
emptyMessage: document.querySelector("#empty-message"),
totalCount: document.querySelector("#total-count"),
readingCount: document.querySelector("#reading-count"),
finishedCount: document.querySelector("#finished-count"),
};
const elements = {
form: document.querySelector('#book-form'),
tbody: document.querySelector('#book-table-body'),
emptyMessage: document.querySelector('#empty-message'),
totalCount: document.querySelector('#total-count'),
readingCount: document.querySelector('#reading-count'),
finishedCount: document.querySelector('#finished-count'),
formErrors: document.querySelector('#form-errors'),
apiError: document.querySelector('#api-error'),
loadingMessage: document.querySelector('#loading-message'),
submitButton: document.querySelector('#submit-button'),
title: document.querySelector('#title'),
author: document.querySelector('#author'),
category: document.querySelector('#category'),
price: document.querySelector('#price'),
status: document.querySelector('#status'),
memo: document.querySelector('#memo'),
};

10-3 の state.errorMessage を 1 つだけ持つ形から、10-5 では 入力エラーAPI エラーローディング状態 を分けて扱う。

変更する場所: 既存の render() 関数を以下の内容に置き換える。elements.errorstate.errorMessage の参照を取り除く。

script.js
function render() {
if (state.errorMessage) {
elements.error.hidden = false;
elements.error.textContent = state.errorMessage;
} else {
elements.error.hidden = true;
elements.error.textContent = "";
}
renderSummary();
renderBooks();
}
function render() {
renderSummary();
renderBooks();
}

elements.error#form-error)は HTML から削除済み、state.errorMessagestate から取り除いたため、ここで参照を消す。エラー表示は 5-2 以降に追加する renderFormErrors() / setApiError() で担う。

  • ブラウザを更新し、Console にエラーが出ていないことを確認する
  • Console で elements.formErrorselements.apiErrorelements.loadingMessage を実行し、どれも null ではないことを確認する
  • まだ見た目が変わらなくてもよい

5-2. 入力エラー表示用の renderFormErrors() を追加する

Section titled “5-2. 入力エラー表示用の renderFormErrors() を追加する”

追加する場所: script.js の末尾に追加する。

script.js
function renderFormErrors(errors) {
if (errors.length === 0) {
elements.formErrors.hidden = true;
elements.formErrors.textContent = '';
return;
}
elements.formErrors.hidden = false;
elements.formErrors.textContent = errors.join(' / ');
}
  • Console で renderFormErrors(['タイトルは必須です']) を実行し、フォーム近くに文言が出ることを確認する
  • 続けて renderFormErrors([]) を実行し、メッセージが消えることを確認する

5-3. API エラー表示用の setApiError() を追加する

Section titled “5-3. API エラー表示用の setApiError() を追加する”

追加する場所: script.js の末尾に追加する。

script.js
function setApiError(message) {
state.apiErrorMessage = message;
elements.apiError.textContent = message;
elements.apiError.hidden = !message;
}
  • Console で setApiError('API エラーのテスト') を実行するとエラー文言が表示される
  • setApiError('') で消える

5-4. ローディング表示用の setLoading() を追加する

Section titled “5-4. ローディング表示用の setLoading() を追加する”

追加する場所: script.js の末尾に追加する。

script.js
function setLoading(isLoading, message = '通信中です...') {
state.isLoading = isLoading;
elements.loadingMessage.textContent = isLoading ? message : '';
elements.loadingMessage.hidden = !isLoading;
elements.submitButton.disabled = isLoading;
}

通信成功時だけ setLoading(false) を書くと、失敗したときにローディング表示が消えずに残る。
だから非同期処理では、最後に必ず通る finally が重要になる。

  • Console で setLoading(true, 'テスト中です...') を実行すると、ローディング文言が表示されて送信ボタンが無効化される
  • 続けて setLoading(false) を実行すると、文言が消えてボタンも元に戻る

ここで見たいのは、通信前でも UI の補助表示を 1 つずつ出し分けられるようになったか である。


画面読み込み時には API から一覧を取ってくる。

追加する場所: script.js の末尾に追加する。

script.js
async function loadBooks() {
setLoading(true, '書籍一覧を読み込んでいます...');
setApiError('');
try {
const books = await requestJSON(API_BASE_URL);
state.books = books;
render();
} catch (error) {
console.error(error);
setApiError(error.message);
} finally {
setLoading(false);
}
}

10-3 ではブラウザ内の state を同期的に更新できた。
しかし fetch() は待ち時間があるので、

loadBooks()
await requestJSON(...)
返ってくるまで待つ

という流れになる。
そのため、ローディング表示が必要になる。

  • この段階では、ページ更新だけではまだ自動取得されなくてもよい
  • ブラウザを更新したあと、Console で loadBooks() を 1 回実行する
  • Network タブで GET /api/books200 になっていることを確認する
  • 一覧テーブルに書籍が表示されることを確認する

まだ init() を API 版へつないでいないので、手動で一覧取得を試す段階 だと考えると整理しやすい。


7. 追加 (POST) を 2 ステップでつなぐ

Section titled “7. 追加 (POST) を 2 ステップでつなぐ”

入力チェックは 10-3 の validateBook() をそのまま使ってよい。
違うのは、追加先が state だけではなく API になることだ。

追加する場所: createBookOnServer()readBookFormData() の下に追加する。readBookFormData()resetForm() は既存のものをそのまま使えるなら残してよい。

script.js
async function createBookOnServer(bookInput) {
return requestJSON(API_BASE_URL, {
method: 'POST',
body: JSON.stringify(bookInput),
});
}
  1. ブラウザを更新する
  2. Console で次を実行する
createBookOnServer({
title: 'POST確認用',
author: '研修 太郎',
category: 'フロントエンド',
price: 2800,
status: '未読',
memo: 'helper の確認',
}).then((book) => console.log(book));
  1. Network タブで POST /api/books201 で返ることを確認する
  2. Console に作成された本のデータが出ることを確認する
  3. まだ一覧は自動更新されなくてよい

変更する場所: 既存の submit イベントハンドラを削除し、緑の async ハンドラへ置き換える。

script.js
elements.form.addEventListener('submit', (event) => {
event.preventDefault();
const formData = readBookFormData();
const errors = validateBook(formData);
if (errors.length > 0) {
state.errorMessage = errors[0];
render();
return;
}
state.errorMessage = '';
state.books.unshift({
id: 'b' + Date.now(),
...formData,
});
render();
elements.form.reset();
});
elements.form.addEventListener('submit', async (event) => {
event.preventDefault();
const formData = readBookFormData();
const errors = validateBook(formData);
renderFormErrors(errors);
if (errors.length > 0) {
return;
}
setLoading(true, '書籍を保存しています...');
setApiError('');
try {
const createdBook = await createBookOnServer(formData);
state.books.unshift(createdBook);
render();
resetForm();
} catch (error) {
console.error(error);
setApiError(error.message);
} finally {
setLoading(false);
}
});

「送ったデータをそのまま state に入れればよい」と思うかもしれない。
しかし実際には、サーバー側で id を採番したり、値を補正したりすることがある。

そのため、画面へ反映するのはサーバーが返した確定版データ にするのが安全である。

  1. ブラウザを更新する
  2. 一覧が空なら、Console で loadBooks() を実行して初期データを読み込む
  3. フォームへ新しい本の情報を入力して送信する
  4. Network タブで POST /api/books201 で返ることを確認する
  5. レスポンス本文に新しい本のデータが入っていることを確認する
  6. 一覧に新しい行が追加され、フォームの入力欄が初期状態へ戻ることを確認する

8. 状態変更 (PATCH) を 3 ステップでつなぐ

Section titled “8. 状態変更 (PATCH) を 3 ステップでつなぐ”

一覧の状態欄をバッジから select へ変えると、更新練習がしやすい。

変更する場所: 既存の createStatusCell() を削除し、緑の createStatusCell() へ置き換える。

script.js
function createStatusCell(status) {
const td = document.createElement('td');
const badge = document.createElement('span');
badge.className = `status-badge ${getStatusClass(status)}`;
badge.textContent = status;
td.appendChild(badge);
return td;
}
function createStatusCell(book) {
const td = document.createElement('td');
const select = document.createElement('select');
select.className = 'status-select';
select.dataset.bookId = book.id;
['未読', '読書中', '読了'].forEach((status) => {
const option = document.createElement('option');
option.value = status;
option.textContent = status;
option.selected = status === book.status;
select.appendChild(option);
});
td.appendChild(select);
return td;
}

変更する場所: renderBooks() 内の createStatusCell の呼び出しも合わせて変更する。

script.js
tr.appendChild(createStatusCell(book.status));
tr.appendChild(createStatusCell(book));

createStatusCell の引数が status 文字列から book オブジェクト全体に変わったため、呼び出し側も合わせる必要がある。これを変えないと select.dataset.bookId がセットされず、変更ハンドラで bookIdundefined になる。

  1. ブラウザを更新する
  2. 一覧が空なら、Console で loadBooks() を実行する
  3. 状態欄がバッジではなく select になっていることを確認する
  4. まだ変更してもサーバーへは送られなくてよい

追加する場所: script.js の末尾に追加する。

script.js
async function updateBookStatus(bookId, nextStatus) {
const updatedBook = await requestJSON(`${API_BASE_URL}/${bookId}`, {
method: 'PATCH',
body: JSON.stringify({ status: nextStatus }),
});
state.books = state.books.map((book) =>
String(book.id) === String(bookId) ? updatedBook : book
);
render();
}
  1. ブラウザを更新する
  2. 一覧が空なら、Console で loadBooks() を実行する
  3. Console で updateBookStatus(state.books[0].id, '読了') を実行する
  4. Network タブで PATCH /api/books/{id}200 で返ることを確認する
  5. 一覧の先頭行の状態表示も変わることを確認する

select は利用者が操作すると、見た目上は先に値が変わる。
もし API 更新が失敗したら、画面は元の値へ戻したい。

state はまだ成功版のままなので、render() を呼び直せば表示も正しい状態へ戻せる。

8-3. change ハンドラをつないで画面操作で更新できるようにする

Section titled “8-3. change ハンドラをつないで画面操作で更新できるようにする”

追加する場所: tbody の change ハンドラを script.js の末尾に追加する。

script.js
elements.tbody.addEventListener('change', async (event) => {
const target = event.target;
if (!(target instanceof HTMLSelectElement)) {
return;
}
if (!target.matches('.status-select')) {
return;
}
const bookId = target.dataset.bookId;
const nextStatus = target.value;
setLoading(true, '状態を更新しています...');
setApiError('');
try {
await updateBookStatus(bookId, nextStatus);
} catch (error) {
console.error(error);
setApiError(error.message);
render();
} finally {
setLoading(false);
}
});
  1. ブラウザを更新する
  2. 一覧が空なら、Console で loadBooks() を実行する
  3. 1 行の状態を select で変更する
  4. Network タブで PATCH /api/books/{id}200 で返ることを確認する
  5. 一覧の表示もその状態へ変わることを確認する

失敗時は、エラー表示が出て render() により表示が元へ戻ることも観察ポイントになる。


9. 削除 (DELETE) を 2 ステップでつなぐ

Section titled “9. 削除 (DELETE) を 2 ステップでつなぐ”

削除では、204 No Content が返る API も多い。
だから requestJSON() では、204 のとき null を返すようにしていた。

追加する場所: deleteBookOnServer()script.js の末尾に追加する。

script.js
async function deleteBookOnServer(bookId) {
await requestJSON(`${API_BASE_URL}/${bookId}`, {
method: 'DELETE',
});
state.books = state.books.filter((book) => String(book.id) !== String(bookId));
render();
}
  1. ブラウザを更新する
  2. 一覧が空なら、Console で loadBooks() を実行する
  3. Console で deleteBookOnServer(state.books[state.books.length - 1].id) を実行する
  4. Network タブで DELETE /api/books/{id}204 で返ることを確認する
  5. テーブルから対象行が消え、件数表示も減ることを確認する

9-2. 削除ボタンを API 版 click ハンドラへ置き換える

Section titled “9-2. 削除ボタンを API 版 click ハンドラへ置き換える”

変更する場所: 10-3 の既存の tbody click ハンドラを削除し、緑の delete 用 click ハンドラへ置き換える。

script.js
elements.tbody.addEventListener('click', (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) return;
const actionButton = target.closest('button[data-action]');
if (!(actionButton instanceof HTMLButtonElement)) return;
const bookId = actionButton.dataset.bookId;
const action = actionButton.dataset.action;
if (action === 'toggle-status') {
state.books = state.books.map((book) =>
book.id === bookId
? { ...book, status: nextStatus(book.status) }
: book
);
render();
return;
}
if (action === 'delete') {
state.books = state.books.filter((book) => book.id !== bookId);
render();
}
});
elements.tbody.addEventListener('click', async (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) {
return;
}
if (!target.matches('.delete-button')) {
return;
}
const bookId = target.dataset.bookId;
setLoading(true, '書籍を削除しています...');
setApiError('');
try {
await deleteBookOnServer(bookId);
} catch (error) {
console.error(error);
setApiError(error.message);
} finally {
setLoading(false);
}
});
  1. ブラウザを更新する
  2. 一覧が空なら、Console で loadBooks() を実行する
  3. どれか 1 行の削除ボタンを押す
  4. Network タブで DELETE /api/books/{id}204 で返ることを確認する
  5. テーブルから対象行が消え、件数表示も減ることを確認する

10. 初期化処理を API 読み込みへ置き換える

Section titled “10. 初期化処理を API 読み込みへ置き換える”

最後に、10-3 までの初期化処理を API 版へ差し替える。

変更する場所: 既存の render(); を削除し、緑の init()init(); へ置き換える。

script.js
render();
function init() {
renderFormErrors([]);
render();
loadBooks();
}
init();

最初に空画面を描いてから loadBooks() を走らせると、

  • 通信前の UI
  • 通信中のメッセージ
  • 通信後の一覧

の切り替わりが分かりやすい。

  • ブラウザを更新した直後に GET /api/books が自動で飛ぶことを Network タブで確認する
  • 読み込み中メッセージが出てから消え、一覧が表示されることを確認する
  • ここまで来たら、以後は Console で loadBooks() を手で呼ばなくてよい

API 連携で最も大切なのは、「コードの想像」ではなく「実際に飛んだ通信」を見ること である。

Network
├─ Name : /api/books など
├─ Method : GET / POST / PATCH / DELETE
├─ Status : 200 / 201 / 204 / 400 / 500
├─ Payload : 送った JSON
└─ Response : 返ってきた JSON やエラーメッセージ
  1. DevTools を開いて Network タブへ移動する
  2. Fetch/XHR フィルタを選ぶ
  3. ページを再読み込みして GET /api/books を確認する
  4. フォーム送信して POST /api/books を確認する
  5. 行の状態を変えて PATCH /api/books/:id を確認する
  6. 削除して DELETE /api/books/:id を確認する
  • Headers: URL や HTTP メソッドが合っているか
  • Payload: 送信 JSON に titleprice が入っているか
  • Response: サーバーが何を返したか
  • Preview: 返却データの形が state と合っているか

Network タブは「サーバーが悪い」「フロントが悪い」と決めつける前に、事実を確認する場所 である。


失敗症状直し方
await を忘れるPromise のまま state へ入って画面が壊れるfetch の結果を使う箇所は await する
response.ok を確認していない400/500 でも成功したように見える共通関数で ok を見て Error を投げる
JSON.stringify せずに body へオブジェクトを入れるサーバー側で JSON として解釈できないbody: JSON.stringify(formData) にする
finallysetLoading(false) していないエラー後もずっと「通信中」のままになる成功/失敗に関係なく最後で戻す
DELETE 後に response.json() を必ず読もうとする204 No Content でエラーになる204 は本文なしとして扱う
API URL や CORS 設定が合っていないNetwork に失敗が出る、Console に CORS エラーが出るhttp://localhost:8080/api/books になっているか、10-4 の @CrossOrigin を入れたか確認する

1. Console だけでなく Network をセットで見る

Section titled “1. Console だけでなく Network をセットで見る”

Console のエラーだけでは「どの URL に、どんな body を送り、何が返ったか」までは分からない。
API 連携では、

Console = JavaScript がどう感じたか
Network = 実際の通信がどうだったか

の両方を見る必要がある。

問題の切り分けが難しいときは、通信成功時の返り値を一度 console.log() で見る。

const books = await requestJSON(API_BASE_URL);
console.log(books);

state.books に入れる前のデータ形を確認すると、「配列のはずがオブジェクトだった」のようなズレに気づきやすい。

3. 失敗時は status code から考える

Section titled “3. 失敗時は status code から考える”
  • 400 台: 送信値や URL の問題が多い
  • 401 / 403: 認証・認可の問題が多い
  • 404: パスの間違いが多い
  • 500 台: サーバー側の処理失敗が多い

このように、HTTP ステータスは「どこを疑うべきか」のヒントになる。

4. state の変化と通信の順序を追う

Section titled “4. state の変化と通信の順序を追う”
submit / change / click
setLoading(true)
await requestJSON(...)
state.books 更新
render()
setLoading(false)

この順番のどこで止まっているかを意識すると、非同期処理も整理しやすい。


項目ポイント
fetchブラウザから HTTP リクエストを送る標準 API
async / await非同期処理を上から順に読める形で書ける
GET / POST / PATCH / DELETE読む・作る・一部更新・削除を表す
response.okHTTP 的に成功かどうかを判断する
ローディング表示通信中であることを利用者へ伝える
エラー表示失敗理由を画面に出して次の行動を分かりやすくする
Network タブ実際に飛んだ通信の URL・Method・Payload・Response を確認する

演習問題 で、HTTP メソッド・非同期処理・Network タブの見方を自分で確認しよう。

その後は 10-6. JPA/Hibernateでデータを永続化する へ進み、いま接続した API の保存先を本当のデータベースへ切り替えよう。