10-5. fetchでフロントエンドとAPIを接続する
このセクションで学ぶこと
Section titled “このセクションで学ぶこと”fetchとasync/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/ が開けることを確認してから次へ進む。
サーバーが停止している場合
Section titled “サーバーが停止している場合”ターミナルを閉じるなどしてサーバーが止まっている場合は、book-app/book-frontend/ で次を再実行する。
npx --yes serve . --listen 4173このセクションの終わりでは、次の動きができる状態を目指す。
- 画面読み込み時に API から書籍一覧を取得できる
- フォーム送信で
POSTし、新しい本を追加できる - 状態変更を
PATCHでサーバーへ反映できる - 削除を
DELETEで実行できる - 通信中はローディング表示を出し、失敗時はエラー理由を画面へ表示できる
- Network タブで「どの通信が、どの結果になったか」を追える
ブラウザ UI ↓ fetchREST API (/api/books) ↓サーバー側の保存処理 ↓レスポンス(JSON) ↓state.books を更新 ↓render()このセクションの進め方
Section titled “このセクションの進め方”10-5 は、script.js を保存 → ブラウザを更新 → Network と Console で何が起きたか確認する という流れで進めると理解しやすい。
このセクションでは、各実装段階の末尾に「ブラウザを更新して確認する」を置く。毎回、次の順で進めよう。
- 指定された HTML / CSS / JavaScript を追加・修正する
- 保存する
- ブラウザを更新する
- 画面・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)を以下の内容に更新する。赤い行(-)が変更前、緑の行(+)が変更後または新規追加。
<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(ローディング)
ブラウザを更新して確認する
Section titled “ブラウザを更新して確認する”- ブラウザを更新しても、
form-errors、api-error、loading-messageはhiddenなのでまだ見えない - Elements タブで
#form-errorが消えて#form-errors、#api-error、#loading-messageがフォームの近くに追加されていることを確認する
この段階では、通信状態を出す置き場だけ先に作った と考えればよい。
3-2. CSS を追加する
Section titled “3-2. CSS を追加する”通信中メッセージと、あとで使う状態選択 select の見た目も先に整えておく。
追加する場所: 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 “なぜローディング表示が必要なのか”通信は一瞬で終わるとは限らない。
何も表示しないと、利用者からは「押せていないのか」「待てばよいのか」が分からない。
ボタンを押した ↓通信中表示がある → いま処理中だと分かる通信中表示がない → 反応していないように見えるブラウザを更新して確認する
Section titled “ブラウザを更新して確認する”- ブラウザを更新し、見た目が大きく変わらなくてもよい
.message-infoはloading-messageがまだ hidden のため見えない.status-selectも、まだ状態欄をselectにしていないので見えなくてよい
この段階では、あとで見える UI の見た目だけ先に仕込んだ と整理すればよい。
4. fetch を共通化する requestJSON() を作る
Section titled “4. fetch を共通化する requestJSON() を作る”毎回 fetch() の結果確認を書くと長くなるので、共通関数を 1 つ用意する。
追加する場所: 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;}なぜ response.ok を確認するのか
Section titled “なぜ response.ok を確認するのか”fetch() は、HTTP 400 や 500 でも「通信自体は返ってきた」と見なして Promise を解決する。
つまり await fetch() だけでは、成功か失敗かを判断しきれない。
通信できた = Promise は解決される業務的に成功した = response.ok を見ないと分からないこの違いを理解しておくと、「なぜ catch に入らないのに失敗しているのか」で迷いにくい。
ブラウザを更新して確認する
Section titled “ブラウザを更新して確認する”- ブラウザを更新し、Console に構文エラーが出ていないことを確認する
- まだ画面は変わらなくてよい
requestJSON()はまだ他の処理から呼ばれていないので、共通関数の土台だけが入った状態 である
5. ローディングとエラー表示の土台を 1 つずつ作る
Section titled “5. ローディングとエラー表示の土台を 1 つずつ作る”通信は非同期なので、画面へ状態を出す関数を分けておく。
5-1. state と elements を広げる
Section titled “5-1. state と elements を広げる”変更する場所: 既存の state ブロックを削除し、緑の state ブロックへ置き換える。
const state = { books: [ { id: 'b1', title: 'リーダブルコード', author: 'Dustin Boswell', category: '設計', price: 3200, status: '未読', memo: '第1章から読む', }, ], errorMessage: '',};
const state = { books: [], isLoading: false, apiErrorMessage: '',};変更する場所: 既存の elements ブロックを削除し、緑の elements ブロックへ置き換える。
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.error と state.errorMessage の参照を取り除く。
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.errorMessage も state から取り除いたため、ここで参照を消す。エラー表示は 5-2 以降に追加する renderFormErrors() / setApiError() で担う。
ブラウザを更新して確認する
Section titled “ブラウザを更新して確認する”- ブラウザを更新し、Console にエラーが出ていないことを確認する
- Console で
elements.formErrors、elements.apiError、elements.loadingMessageを実行し、どれもnullではないことを確認する - まだ見た目が変わらなくてもよい
5-2. 入力エラー表示用の renderFormErrors() を追加する
Section titled “5-2. 入力エラー表示用の renderFormErrors() を追加する”追加する場所: 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(' / ');}ブラウザを更新して確認する
Section titled “ブラウザを更新して確認する”- Console で
renderFormErrors(['タイトルは必須です'])を実行し、フォーム近くに文言が出ることを確認する - 続けて
renderFormErrors([])を実行し、メッセージが消えることを確認する
5-3. API エラー表示用の setApiError() を追加する
Section titled “5-3. API エラー表示用の setApiError() を追加する”追加する場所: script.js の末尾に追加する。
function setApiError(message) { state.apiErrorMessage = message; elements.apiError.textContent = message; elements.apiError.hidden = !message;}ブラウザを更新して確認する
Section titled “ブラウザを更新して確認する”- Console で
setApiError('API エラーのテスト')を実行するとエラー文言が表示される setApiError('')で消える
5-4. ローディング表示用の setLoading() を追加する
Section titled “5-4. ローディング表示用の setLoading() を追加する”追加する場所: script.js の末尾に追加する。
function setLoading(isLoading, message = '通信中です...') { state.isLoading = isLoading; elements.loadingMessage.textContent = isLoading ? message : ''; elements.loadingMessage.hidden = !isLoading; elements.submitButton.disabled = isLoading;}finally が必要になる理由
Section titled “finally が必要になる理由”通信成功時だけ setLoading(false) を書くと、失敗したときにローディング表示が消えずに残る。
だから非同期処理では、最後に必ず通る finally が重要になる。
ブラウザを更新して確認する
Section titled “ブラウザを更新して確認する”- Console で
setLoading(true, 'テスト中です...')を実行すると、ローディング文言が表示されて送信ボタンが無効化される - 続けて
setLoading(false)を実行すると、文言が消えてボタンも元に戻る
ここで見たいのは、通信前でも UI の補助表示を 1 つずつ出し分けられるようになったか である。
6. 一覧取得 (GET) を実装する
Section titled “6. 一覧取得 (GET) を実装する”画面読み込み時には API から一覧を取ってくる。
追加する場所: 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 との違い
Section titled “10-3 との違い”10-3 ではブラウザ内の state を同期的に更新できた。
しかし fetch() は待ち時間があるので、
loadBooks() ↓await requestJSON(...) ↓返ってくるまで待つという流れになる。
そのため、ローディング表示が必要になる。
ブラウザを更新して確認する
Section titled “ブラウザを更新して確認する”- この段階では、ページ更新だけではまだ自動取得されなくてもよい
- ブラウザを更新したあと、Console で
loadBooks()を 1 回実行する - Network タブで
GET /api/booksが200になっていることを確認する - 一覧テーブルに書籍が表示されることを確認する
まだ init() を API 版へつないでいないので、手動で一覧取得を試す段階 だと考えると整理しやすい。
7. 追加 (POST) を 2 ステップでつなぐ
Section titled “7. 追加 (POST) を 2 ステップでつなぐ”入力チェックは 10-3 の validateBook() をそのまま使ってよい。
違うのは、追加先が state だけではなく API になることだ。
7-1. createBookOnServer() を追加する
Section titled “7-1. createBookOnServer() を追加する”追加する場所: createBookOnServer() を readBookFormData() の下に追加する。readBookFormData() と resetForm() は既存のものをそのまま使えるなら残してよい。
async function createBookOnServer(bookInput) { return requestJSON(API_BASE_URL, { method: 'POST', body: JSON.stringify(bookInput), });}ブラウザを更新して確認する
Section titled “ブラウザを更新して確認する”- ブラウザを更新する
- Console で次を実行する
createBookOnServer({ title: 'POST確認用', author: '研修 太郎', category: 'フロントエンド', price: 2800, status: '未読', memo: 'helper の確認',}).then((book) => console.log(book));- Network タブで
POST /api/booksが201で返ることを確認する - Console に作成された本のデータが出ることを確認する
- まだ一覧は自動更新されなくてよい
7-2. submit を API 版へ置き換える
Section titled “7-2. submit を API 版へ置き換える”変更する場所: 既存の submit イベントハンドラを削除し、緑の async ハンドラへ置き換える。
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); }});なぜ API の返り値を使うのか
Section titled “なぜ API の返り値を使うのか”「送ったデータをそのまま state に入れればよい」と思うかもしれない。
しかし実際には、サーバー側で id を採番したり、値を補正したりすることがある。
そのため、画面へ反映するのはサーバーが返した確定版データ にするのが安全である。
ブラウザを更新して確認する
Section titled “ブラウザを更新して確認する”- ブラウザを更新する
- 一覧が空なら、Console で
loadBooks()を実行して初期データを読み込む - フォームへ新しい本の情報を入力して送信する
- Network タブで
POST /api/booksが201で返ることを確認する - レスポンス本文に新しい本のデータが入っていることを確認する
- 一覧に新しい行が追加され、フォームの入力欄が初期状態へ戻ることを確認する
8. 状態変更 (PATCH) を 3 ステップでつなぐ
Section titled “8. 状態変更 (PATCH) を 3 ステップでつなぐ”一覧の状態欄をバッジから select へ変えると、更新練習がしやすい。
8-1. 状態欄を select に変える
Section titled “8-1. 状態欄を select に変える”変更する場所: 既存の createStatusCell() を削除し、緑の createStatusCell() へ置き換える。
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 の呼び出しも合わせて変更する。
tr.appendChild(createStatusCell(book.status)); tr.appendChild(createStatusCell(book));createStatusCell の引数が status 文字列から book オブジェクト全体に変わったため、呼び出し側も合わせる必要がある。これを変えないと select.dataset.bookId がセットされず、変更ハンドラで bookId が undefined になる。
ブラウザを更新して確認する
Section titled “ブラウザを更新して確認する”- ブラウザを更新する
- 一覧が空なら、Console で
loadBooks()を実行する - 状態欄がバッジではなく
selectになっていることを確認する - まだ変更してもサーバーへは送られなくてよい
8-2. updateBookStatus() を追加する
Section titled “8-2. updateBookStatus() を追加する”追加する場所: 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();}ブラウザを更新して確認する
Section titled “ブラウザを更新して確認する”- ブラウザを更新する
- 一覧が空なら、Console で
loadBooks()を実行する - Console で
updateBookStatus(state.books[0].id, '読了')を実行する - Network タブで
PATCH /api/books/{id}が200で返ることを確認する - 一覧の先頭行の状態表示も変わることを確認する
失敗時に render() し直す理由
Section titled “失敗時に render() し直す理由”select は利用者が操作すると、見た目上は先に値が変わる。
もし API 更新が失敗したら、画面は元の値へ戻したい。
state はまだ成功版のままなので、render() を呼び直せば表示も正しい状態へ戻せる。
8-3. change ハンドラをつないで画面操作で更新できるようにする
Section titled “8-3. change ハンドラをつないで画面操作で更新できるようにする”追加する場所: tbody の change ハンドラを 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); }});ブラウザを更新して確認する
Section titled “ブラウザを更新して確認する”- ブラウザを更新する
- 一覧が空なら、Console で
loadBooks()を実行する - 1 行の状態を
selectで変更する - Network タブで
PATCH /api/books/{id}が200で返ることを確認する - 一覧の表示もその状態へ変わることを確認する
失敗時は、エラー表示が出て render() により表示が元へ戻ることも観察ポイントになる。
9. 削除 (DELETE) を 2 ステップでつなぐ
Section titled “9. 削除 (DELETE) を 2 ステップでつなぐ”削除では、204 No Content が返る API も多い。
だから requestJSON() では、204 のとき null を返すようにしていた。
9-1. deleteBookOnServer() を追加する
Section titled “9-1. deleteBookOnServer() を追加する”追加する場所: deleteBookOnServer() を 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();}ブラウザを更新して確認する
Section titled “ブラウザを更新して確認する”- ブラウザを更新する
- 一覧が空なら、Console で
loadBooks()を実行する - Console で
deleteBookOnServer(state.books[state.books.length - 1].id)を実行する - Network タブで
DELETE /api/books/{id}が204で返ることを確認する - テーブルから対象行が消え、件数表示も減ることを確認する
9-2. 削除ボタンを API 版 click ハンドラへ置き換える
Section titled “9-2. 削除ボタンを API 版 click ハンドラへ置き換える”変更する場所: 10-3 の既存の tbody click ハンドラを削除し、緑の delete 用 click ハンドラへ置き換える。
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); }});ブラウザを更新して確認する
Section titled “ブラウザを更新して確認する”- ブラウザを更新する
- 一覧が空なら、Console で
loadBooks()を実行する - どれか 1 行の削除ボタンを押す
- Network タブで
DELETE /api/books/{id}が204で返ることを確認する - テーブルから対象行が消え、件数表示も減ることを確認する
10. 初期化処理を API 読み込みへ置き換える
Section titled “10. 初期化処理を API 読み込みへ置き換える”最後に、10-3 までの初期化処理を API 版へ差し替える。
変更する場所: 既存の render(); を削除し、緑の init() と init(); へ置き換える。
render();
function init() { renderFormErrors([]); render(); loadBooks();}
init();最初に空画面を描いてから loadBooks() を走らせると、
- 通信前の UI
- 通信中のメッセージ
- 通信後の一覧
の切り替わりが分かりやすい。
ブラウザを更新して確認する
Section titled “ブラウザを更新して確認する”- ブラウザを更新した直後に
GET /api/booksが自動で飛ぶことを Network タブで確認する - 読み込み中メッセージが出てから消え、一覧が表示されることを確認する
- ここまで来たら、以後は Console で
loadBooks()を手で呼ばなくてよい
11. Network タブで通信を調べる
Section titled “11. Network タブで通信を調べる”API 連携で最も大切なのは、「コードの想像」ではなく「実際に飛んだ通信」を見ること である。
見るべき場所
Section titled “見るべき場所”Network├─ Name : /api/books など├─ Method : GET / POST / PATCH / DELETE├─ Status : 200 / 201 / 204 / 400 / 500├─ Payload : 送った JSON└─ Response : 返ってきた JSON やエラーメッセージ典型的な確認手順
Section titled “典型的な確認手順”- DevTools を開いて Network タブへ移動する
Fetch/XHRフィルタを選ぶ- ページを再読み込みして
GET /api/booksを確認する - フォーム送信して
POST /api/booksを確認する - 行の状態を変えて
PATCH /api/books/:idを確認する - 削除して
DELETE /api/books/:idを確認する
どこを見ればよいか
Section titled “どこを見ればよいか”- Headers: URL や HTTP メソッドが合っているか
- Payload: 送信 JSON に
titleやpriceが入っているか - Response: サーバーが何を返したか
- Preview: 返却データの形が state と合っているか
Network タブは「サーバーが悪い」「フロントが悪い」と決めつける前に、事実を確認する場所 である。
よくある失敗
Section titled “よくある失敗”| 失敗 | 症状 | 直し方 |
|---|---|---|
await を忘れる | Promise のまま state へ入って画面が壊れる | fetch の結果を使う箇所は await する |
response.ok を確認していない | 400/500 でも成功したように見える | 共通関数で ok を見て Error を投げる |
JSON.stringify せずに body へオブジェクトを入れる | サーバー側で JSON として解釈できない | body: JSON.stringify(formData) にする |
finally で setLoading(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 = 実際の通信がどうだったかの両方を見る必要がある。
2. fetch の結果をログに出す
Section titled “2. fetch の結果をログに出す”問題の切り分けが難しいときは、通信成功時の返り値を一度 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.ok | HTTP 的に成功かどうかを判断する |
| ローディング表示 | 通信中であることを利用者へ伝える |
| エラー表示 | 失敗理由を画面に出して次の行動を分かりやすくする |
| Network タブ | 実際に飛んだ通信の URL・Method・Payload・Response を確認する |
次のステップ
Section titled “次のステップ”演習問題 で、HTTP メソッド・非同期処理・Network タブの見方を自分で確認しよう。
その後は 10-6. JPA/Hibernateでデータを永続化する へ進み、いま接続した API の保存先を本当のデータベースへ切り替えよう。