UIからAPIを呼び出す
概要
- 日程: Day 4 / セッション 3
- 時間: [10:45-12:00]
- 形式: 実習
- ゴール: JavaScript(fetch)でモックAPIを呼び出し、取得した JSON を画面に一覧表示できる。通信を api.js に、描画を app.js に分けた構成を保ったまま実装できる
- 学習形式: ハンズオン実習(AIペアプログラミング)
導入(5分)
前のセッションで機能側(モックAPI)が動きました。curl で叩けば JSON が返ってきます。
ここで少し考えてみてください。curl がやっていることを、ブラウザの JavaScript にやらせたら何が起きるでしょうか? そう、「画面にデータが出る」。つまりアプリケーションの誕生です。
このセッションは「プレゼンテーションと機能の分離」の実践編です。Day 2 で描いたワイヤーフレームが、API から届く JSON を着て動き出します。ポイントはただ一つ、UI のコードに機能(ビジネスロジック)を混ぜないこと。書きながら何度も確認します。
本編(20分+実習50分)
1. fetch API 3行でHTTPクライアントになる
fetch は、ブラウザに標準搭載された HTTP クライアントです。curl でやったことがそのまま書けます。
const response = await fetch("http://localhost:3000/expenses");
const expenses = await response.json();
console.log(expenses); // JSONが配列として手に入る
fetch(url)がリクエストを送る(既定は GET)response.json()がレスポンスボディを JavaScript のオブジェクトに変換する- どちらも時間がかかる処理なので
awaitで待つ
たとえるなら、fetch は「電話をかける」、await は「相手が出るまで待つ」、response.json() は「聞いた内容をメモに起こす」です。電話をかけた瞬間にメモが手に入るわけではない、という順序が重要です。
POST はこう書きます。curl のオプションと1対1で対応していることに注目してください。
await fetch("http://localhost:3000/expenses", {
method: "POST", // curl の -X POST
headers: { "Content-Type": "application/json" }, // curl の -H
body: JSON.stringify({ date: "2026-07-06", categoryId: 1, amount: 620, memo: "コーヒー" }) // curl の -d
});
ここがポイント
awaitを忘れると、データではなく Promise(未来の約束)が手に入りエラーになる。「Promise とコンソールに出たら await 忘れ」と覚えるJSON.stringifyはオブジェクト→文字列。response.json()は文字列→オブジェクト。向きが逆の相棒
コラム
fetch の前任者は XMLHttpRequest(XHR)です。1999年、マイクロソフトが Outlook Web Access のために IE5 へこっそり入れた機能で、当初はほぼ無名でした。2004〜2005年に Gmail と Google Maps が「ページを再読み込みせずに画面が変わる」体験で世界を驚かせ、この技術に Ajax という名前が付いて大流行します。ただ XHR は名前に XML とあるのに JSON 全盛になり、API も使いにくかったため、2015年にモダンな fetch が標準化されました。名前に歴史あり、です。
2. 呼び出しの流れを図で持つ
UI が API を呼んで画面に出すまでの流れです。この図が頭にあると、エラーが起きたときに「どこで止まったか」を切り分けられます。
登場人物の役割分担がそのまま「分離」です。
- api.js: 通信だけを担当。画面のことを何も知らない
- app.js: 描画とイベントだけを担当。URL や HTTP のことを知らない
- モックAPI: 機能側。UI のことを何も知らない
ここがポイント
- エラー時は図の矢印を上から順に疑う。「API は生きてるか(curl で確認)→ 通信は届いたか(開発者ツールの Network タブ)→ 描画は呼ばれたか(console.log)」
- api.js が「窓口の一本化」になっているので、API の URL 変更は api.js の1行を直すだけで済む
コラム
sequenceDiagram のような時系列図のご先祖は、通信プロトコルの世界で使われた「メッセージシーケンスチャート」です。1990年代に UML に取り込まれ、シーケンス図として定着しました。人間同士でも「誰が・誰に・どの順で」話すかを図にすると誤解が減ります。議事録より図1枚、はエンジニアの生活の知恵です。
3. 完全に動くコード例 家計簿の一覧と登録
前セッションの JSON Server(ポート3000)が起動している前提で、そのまま動く一式です。ui/ フォルダに3ファイル作ります。
ui/index.html(プレゼンテーションの構造。ワイヤーフレームの翻訳)
<meta charset="utf-8">
<title>家計簿</title>
<h1>家計簿 支出一覧</h1>
<form id="expense-form">
<input type="date" id="date" required>
<select id="categoryId"></select>
<input type="number" id="amount" placeholder="金額" required min="1">
<input type="text" id="memo" placeholder="メモ">
<button type="submit">登録</button>
</form>
<table border="1">
<thead>
<tr><th>日付</th><th>カテゴリ</th><th>金額</th><th>メモ</th></tr>
</thead>
<tbody id="expense-list"></tbody>
</table>
<p id="total"></p>
<script src="api.js"></script>
<script src="app.js"></script>
ui/api.js(通信の窓口。機能側との契約をここに集約)
const API_BASE = "http://localhost:3000";
async function fetchExpenses() {
const res = await fetch(`${API_BASE}/expenses`);
if (!res.ok) throw new Error(`APIエラー: ${res.status}`);
return res.json();
}
async function fetchCategories() {
const res = await fetch(`${API_BASE}/categories`);
if (!res.ok) throw new Error(`APIエラー: ${res.status}`);
return res.json();
}
async function createExpense(expense) {
const res = await fetch(`${API_BASE}/expenses`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(expense)
});
if (!res.ok) throw new Error(`登録失敗: ${res.status}`);
return res.json();
}
ui/app.js(描画とイベント。HTTP のことは知らない)
async function renderCategories() {
const categories = await fetchCategories();
const select = document.getElementById("categoryId");
select.innerHTML = categories
.map((c) => `<option value="${c.id}">${c.name}</option>`)
.join("");
}
async function renderExpenses() {
const expenses = await fetchExpenses();
const categories = await fetchCategories();
const nameOf = (id) => categories.find((c) => c.id === Number(id))?.name ?? "不明";
const tbody = document.getElementById("expense-list");
tbody.innerHTML = expenses
.map((e) => `<tr>
<td>${e.date}</td><td>${nameOf(e.categoryId)}</td>
<td>${e.amount.toLocaleString()}円</td><td>${e.memo ?? ""}</td>
</tr>`)
.join("");
const total = expenses.reduce((sum, e) => sum + e.amount, 0);
document.getElementById("total").textContent = `合計: ${total.toLocaleString()}円`;
}
document.getElementById("expense-form").addEventListener("submit", async (event) => {
event.preventDefault(); // ページ再読み込みを止める
await createExpense({
date: document.getElementById("date").value,
categoryId: Number(document.getElementById("categoryId").value),
amount: Number(document.getElementById("amount").value),
memo: document.getElementById("memo").value
});
await renderExpenses(); // 登録後に一覧を再描画
});
renderCategories();
renderExpenses();
index.html をブラウザで開けば動きます。フォームから登録すると、一覧と合計が更新され、db.json にもデータが増えます。
ここで一つ白状すると、このコードには「分離の議論の種」が仕込んであります。合計金額の計算(reduce の行)は app.js にあります。表示のための集計なら UI の仕事、業務ルール(例: 予算超過の判定)ならば機能側の仕事。あなたはどちらだと思いますか? AI と議論してみてください。正解を暗記するより、境界線を自分で引けることが大切です。
ここがポイント
- ファイルをダブルクリックで開いて fetch が失敗する場合は CORS やローカルファイル制約が原因のことがある。JSON Server は既定で CORS を許可しているので通常は動くが、詰まったら
npx serve uiなどの簡易HTTPサーバ経由で開く - app.js に URL を書きたくなったら手が滑っている。URL は api.js だけに置く
- 発展: この構成のまま React に移行すると、api.js はそのまま使え、app.js がコンポーネントに変わる。分離しておくと乗り換えが安い
コラム
「表示とロジックを分けろ」は最新の教えではありません。1979年、Smalltalk の開発者トリグヴェ・リーンスカウクが提唱した MVC が元祖で、当時から「モデルはビューを知らない」が鉄則でした。40年以上たっても、フレームワークの名前(MVC、MVVM、React の関数コンポーネント)が変わるだけで、原則は同じです。原則を身につければ、流行が変わっても慌てずに済みます。
💬 AIに聞いてみよう
- 「fetch の結果が Promise {
} と表示される。何を間違えている? 修正方法と、そもそも Promise とは何かを初心者向けに教えて」 - 「この app.js(貼り付ける)に、機能側に置くべきロジックが混ざっていないかレビューして。混ざっていたらどこに移すべきかも提案して」
- 「開発者ツールの Network タブで API 呼び出しを確認する手順を、私の画面(一覧表示)を例に教えて」
実習・演習
課題
- UI の骨組みを作る(20分)
ui/index.htmlとui/api.jsとui/app.jsを作成する(上の例を写経してよい)- 自分の開発テーマのリソース名・属性名に置き換える
- 一覧表示を動かす(15分)
- ブラウザで開き、モックAPIのデータが一覧表示されることを確認する
- 開発者ツールの Network タブで GET リクエストと JSON レスポンスを確認する
- 登録を動かす(15分)
- フォームから POST し、一覧が更新されること、
db.jsonが書き換わることを確認する
- フォームから POST し、一覧が更新されること、
- 分離レビュー(AIペア作業)(残り時間)
- app.js と api.js を AI に貼り、「分離が守れているか」をレビューしてもらう
- 指摘が妥当か自分で判断し、直すか直さないかの理由をメモする
成果物
- ブラウザで一覧表示と登録が動く
ui/一式(index.html、api.js、app.js) - 分離レビューのメモ(AI の指摘と自分の判断1件以上)
ヒント
- まず写経で動かし、その後に自分のテーマへ置き換える。二段階に分けると切り分けが楽
- 画面に何も出ないときは、開発者ツールの Console タブを最初に開く。赤いエラーが道しるべ
- ワイヤーフレーム(Day 2)を横に置き、「この画面のこの欄はどの属性か」を対応させながら作る
まとめ(5分)
今回学んだことを一言でまとめると「UI は fetch で契約(API)を呼ぶだけの存在にする。それがプレゼンテーションと機能の分離の実装形」です。
- fetch は curl の JavaScript 版。GET/POST がそのまま書ける
- api.js(通信)と app.js(描画)を分けると、変更の影響範囲が狭くなる
- エラーはシーケンス図の矢印を上から順に切り分ける
次のセッションでは、この土台を自分の開発テーマに合わせて拡張し、主要ユースケースを通しで動かします。GitHub への push もそこで行います。
🔄 振り返りチェック
- fetch で POST するときに指定する3つの要素(メソッド・ヘッダ・ボディ)を言えますか?
- api.js と app.js の役割分担を「知らない」という言葉を使って説明できますか?
- 画面にデータが出ないとき、最初に確認する場所を2つ挙げられますか?
補足資料
- 参考リンク: MDN「Fetch API の使用」、MDN「JavaScript の非同期処理」、Chrome DevTools 公式ドキュメント(Network タブ)
- 発展課題: 削除ボタンを一覧の各行に追加し、DELETE /expenses/:id を呼んで一覧を更新する。さらに余力があれば、同じ api.js を使って React で一覧画面を書き直し、「api.js が無傷である」ことを確認する
学習ガイド
想定される質問と回答例
| 質問 | ヒント |
|---|---|
| async/await と .then() はどちらを使うべき? | どちらでも動く。この研修では読みやすい async/await に統一。AI に両者の書き換え例を出してもらうと理解が深まる |
| APIのURLをHTMLに書いてはだめ? | 動くが、変更時に探し回ることになる。「URL は api.js の API_BASE だけ」と決めると窓口が一本化される |
| innerHTML での描画は安全? | 学習用途では可。外部入力をそのまま埋め込むと XSS の危険がある。発展課題として AI に textContent や escape の方法を聞いてみる |
つまずきやすいポイント
| つまずきポイント | ヒント |
|---|---|
| await 忘れで Promise { |
fetch と response.json() の前に await があるか確認。関数側にも async が必要。「Promise と出たら await 忘れ」 |
| 画面が真っ白でエラーの場所がわからない | 開発者ツールの Console→Network の順に見る。API が生きているかは curl かブラウザ直叩きで先に確認し、UI と機能のどちら側の問題か切り分ける |
| CORS エラーや file:// 直開きで fetch が失敗する | JSON Server が起動しているか、URL のポートが合っているかを確認。だめなら npx serve ui で簡易HTTPサーバ経由で開く |
| 描画コードの中に URL や計算ルールを書いてしまい分離が崩れる | 「URL は api.js、業務ルールは機能側、表示整形だけ app.js」を合言葉に。書いた後に AI へ分離レビューを依頼する |