ShimaLinkのクライアントが5件に増えた。嬉しい悲鳴だが、新たな問題が浮上した。
あなた: 「ヤバい…Mikaさんのデータを編集してたら、他のクライアントのデータも一緒に壊れちゃった。」
あなた: 「え?全部同じJSONファイルに入ってるから?」
あなた: 「そう…1つのファイルに全クライアントのデータが入ってて、うっかり構造を崩しちゃって。」
Yukiがため息をつく。
Yuki: 「これはもう限界だね。フロントエンドから直接JSONファイルを読み書きする方式じゃ、クライアントが増えるほどリスクが上がる。」
あなた: 「じゃあ、どうするんですか?」
Yuki: 「APIを作る。フロントエンドとデータの間に”架け橋”を置くんだ。この架け橋がルールに従ってデータを出し入れしてくれる。」
Yuki: 「レストランで例えると、お客さんがキッチンに直接入って料理を作るのは危険でしょ?ウェイター(API)を通じて注文し、料理を受け取る。それと同じ。」
あなた: 「APIっていうと、天気予報とかのAPIを使ったことはあるけど…自分で作るってこと?」
Yuki: 「そう。まずはAPIとは何か、HTTPメソッド、そしてJSON形式について理解しよう。それから実際にShimaLink用のAPIを設計する。」
あなたはノートを開いた。WebアプリのバックエンドとフロントエンドをつなぐAPI——その仕組みを学ぶ時間だ。
APIとは何か
APIは “Application Programming Interface” の略。ソフトウェア同士が会話するための「窓口」だ。
「APIはレストランのウェイター。お客さん(フロントエンド)が注文(リクエスト)を出し、キッチン(サーバー)から料理(レスポンス)を受け取る。直接キッチンに入る必要はない。」——Yuki
なぜAPIが必要か
ShimaLinkの現状の問題を整理しよう。
【Before: APIなし】
フロントエンド → 直接JSONファイルを読み書き
問題: 同時アクセスで壊れる、データ形式がバラバラ、セキュリティなし
【After: APIあり】
フロントエンド → API → データ保存
利点: ルールが統一、データの整合性、セキュリティ
Web APIの基本構造
クライアント サーバー
(ブラウザ) (バックエンド)
| |
| HTTPリクエスト |
| =========================> |
| GET /api/clients |
| |
| HTTPレスポンス |
| <========================= |
| { "data": [...] } |
| |
REST API
REST(Representational State Transfer)は、Web APIの設計スタイルの一つです。
RESTの原則
| 原則 | 説明 | ShimaLinkでの例 |
|---|---|---|
| リソース指向 | URLはリソース(データ)を表す | /api/clients |
| HTTPメソッド | 操作の種類をメソッドで表す | GET, POST, PUT, DELETE |
| ステートレス | 各リクエストは独立 | 認証情報も毎回送る |
| 統一インターフェース | 一貫したURL設計 | /api/clients/:id |
URLの設計例
# リソース: クライアント
/api/clients → クライアント全体
/api/clients/1 → ID=1のクライアント
/api/clients/1/reservations → ID=1の予約一覧
# リソース: 予約
/api/reservations → 予約全体
/api/reservations/42 → ID=42の予約
APIの種類
| 種類 | 説明 | 例 |
|---|---|---|
| REST API | URL + HTTPメソッドでCRUD操作 | ShimaLink API |
| GraphQL | クエリ言語でデータ取得 | GitHub API v4 |
| WebSocket | 双方向リアルタイム通信 | チャットアプリ |
ポイント: REST APIが最も広く使われている形式。まずはRESTをしっかり理解しよう。ShimaLinkのAPIもREST形式で作ります。
HTTPメソッド — GET / POST / PUT / DELETE
REST APIでは、HTTPメソッドでデータに対する操作の種類を表す。これはCRUD(作成・読み取り・更新・削除)と対応している。
「HTTPメソッドは”動詞”。URLが”名詞”。この2つで何をしたいかが分かる。」——Yuki
4つの基本メソッド
| メソッド | CRUD | 説明 | リクエスト例 |
|---|---|---|---|
GET | Read | データの取得 | GET /api/clients |
POST | Create | 新規データの作成 | POST /api/clients |
PUT | Update | データの更新(全体) | PUT /api/clients/1 |
DELETE | Delete | データの削除 | DELETE /api/clients/1 |
GET — データを取得する
GET /api/clients HTTP/1.1
Host: api.shimalink.com
---
レスポンス:
HTTP/1.1 200 OK
Content-Type: application/json
[
{ "id": 1, "name": "海風テラス", "category": "カフェ" },
{ "id": 2, "name": "首里そば太郎", "category": "飲食" }
]
// fetchでGETリクエスト(デフォルトはGET)
const response = await fetch("/api/clients");
const clients = await response.json();
POST — 新規作成
POST /api/clients HTTP/1.1
Content-Type: application/json
{ "name": "やんばるキッチン", "category": "飲食" }
---
レスポンス:
HTTP/1.1 201 Created
{ "id": 6, "name": "やんばるキッチン", "category": "飲食" }
const newClient = { name: "やんばるキッチン", category: "飲食" };
const response = await fetch("/api/clients", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newClient)
});
const created = await response.json();
console.log(created.id); // サーバーが割り当てたID
PUT — データを更新
PUT /api/clients/1 HTTP/1.1
Content-Type: application/json
{ "name": "海風テラス", "category": "カフェ", "visits": 1500 }
---
レスポンス:
HTTP/1.1 200 OK
{ "id": 1, "name": "海風テラス", "category": "カフェ", "visits": 1500 }
const updated = {
name: "海風テラス",
category: "カフェ",
visits: 1500
};
const response = await fetch("/api/clients/1", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(updated)
});
DELETE — データを削除
DELETE /api/clients/3 HTTP/1.1
---
レスポンス:
HTTP/1.1 204 No Content
const response = await fetch("/api/clients/3", {
method: "DELETE"
});
if (response.ok) {
console.log("削除成功");
}
ステータスコード
| コード | 意味 | 使いどころ |
|---|---|---|
200 | OK | GET/PUT成功 |
201 | Created | POST成功 |
204 | No Content | DELETE成功 |
400 | Bad Request | リクエスト不正 |
404 | Not Found | リソースが見つからない |
500 | Internal Server Error | サーバー側エラー |
メソッドの特性
| 特性 | GET | POST | PUT | DELETE |
|---|---|---|---|---|
| ボディあり | なし | あり | あり | なし |
| 冪等性 | あり | なし | あり | あり |
| 安全 | はい | いいえ | いいえ | いいえ |
「冪等(べきとう)」とは、同じリクエストを何度送っても結果が同じという意味。GETは何度取得しても同じデータ。PUTは何度更新しても同じ状態。POSTは送るたびに新しいデータが作られる。
ポイント: URLは「何に対して」、HTTPメソッドは「何をするか」を表す。
DELETE /api/clients/3は「ID 3のクライアントを削除する」と読める。
JSON — データの共通言語
APIでやり取りするデータのフォーマットには、JSON(JavaScript Object Notation) が使われる。
「JSONは”データの共通言語”。JavaScript出身だけど、今やどんな言語でも読み書きできる。」——Yuki
JSONとは
JSON はテキストベースのデータ形式で、人間にも機械にも読みやすい。
{
"name": "海風テラス",
"owner": "Mika",
"category": "カフェ",
"monthlyVisits": 1250,
"isActive": true,
"tags": ["オーシャンビュー", "テラス席", "ペットOK"],
"address": {
"prefecture": "沖縄県",
"city": "北谷町",
"detail": "美浜1-2-3"
}
}
JSONのルール
| ルール | 正しい例 | 間違い例 |
|---|---|---|
| キーはダブルクォート必須 | "name" | 'name' や name |
| 文字列はダブルクォート | "hello" | 'hello' |
| 末尾カンマ禁止 | {"a": 1} | {"a": 1,} |
| コメント不可 | — | // コメント |
JSONで使えるデータ型
{
"string": "文字列",
"number": 42,
"float": 3.14,
"boolean": true,
"null": null,
"array": [1, 2, 3],
"object": { "key": "value" }
}
注意: 関数、undefined、Date オブジェクトなどはJSONに含められません。
JavaScriptでのJSON操作
// オブジェクト → JSON文字列
const client = { name: "海風テラス", visits: 1250 };
const jsonString = JSON.stringify(client);
// '{"name":"海風テラス","visits":1250}'
// JSON文字列 → オブジェクト
const parsed = JSON.parse(jsonString);
// { name: "海風テラス", visits: 1250 }
// 整形出力(デバッグ用)
const pretty = JSON.stringify(client, null, 2);
// {
// "name": "海風テラス",
// "visits": 1250
// }
APIでの使い方
リクエスト(送信側)
// POSTでJSONデータを送信
const response = await fetch("/api/clients", {
method: "POST",
headers: {
"Content-Type": "application/json" // JSONであることを宣言
},
body: JSON.stringify({ // オブジェクトをJSON文字列に変換
name: "やんばるキッチン",
category: "飲食"
})
});
レスポンス(受信側)
// レスポンスのJSONをパース
const response = await fetch("/api/clients");
const data = await response.json(); // JSON → オブジェクト
// TypeScriptで型をつける
interface Client {
id: number;
name: string;
category: string;
}
const clients: Client[] = await response.json();
ShimaLink APIのレスポンス設計
統一されたレスポンス形式を定義しよう。
// 成功レスポンス
interface ApiSuccess<T> {
success: true;
data: T;
}
// エラーレスポンス
interface ApiError {
success: false;
error: {
code: string;
message: string;
};
}
type ApiResponse<T> = ApiSuccess<T> | ApiError;
// 成功時
{
"success": true,
"data": [
{ "id": 1, "name": "海風テラス", "category": "カフェ" }
]
}
// エラー時
{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "クライアントが見つかりません"
}
}
ポイント: APIのレスポンス形式を統一することで、フロントエンド側のエラーハンドリングがシンプルになります。
successフラグで成功/失敗を判定する設計は非常に一般的です。
シンプルなAPIを構築する
学んだ知識を統合して、ShimaLink用のAPIを設計・実装してみよう。ここではfetch APIを使ってフロントエンドからAPIを呼び出す実装に焦点を当てる。
「まずはAPIの”使い方”を完璧にしよう。作る側は次のステップだ。」——Yuki
ShimaLink APIの設計
まず、必要なエンドポイントを洗い出す。
ShimaLink API エンドポイント一覧
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GET /api/clients クライアント一覧取得
GET /api/clients/:id 特定のクライアント取得
POST /api/clients 新規クライアント登録
PUT /api/clients/:id クライアント情報更新
DELETE /api/clients/:id クライアント削除
GET /api/clients/:id/reservations 予約一覧取得
POST /api/clients/:id/reservations 予約追加
型定義
// types.ts
interface Client {
id: number;
name: string;
owner: string;
category: "カフェ" | "飲食" | "花屋" | "雑貨" | "その他";
monthlyVisits: number;
isActive: boolean;
createdAt: string;
}
interface Reservation {
id: number;
clientId: number;
guestName: string;
date: string;
partySize: number;
status: "confirmed" | "pending" | "cancelled";
}
interface ApiResponse<T> {
success: boolean;
data: T;
message?: string;
}
APIクライアント関数の実装
// api.ts — ShimaLink APIクライアント
const BASE_URL = "/api";
// 共通のfetch関数
async function apiRequest<T>(
endpoint: string,
options?: RequestInit
): Promise<ApiResponse<T>> {
const response = await fetch(`${BASE_URL}${endpoint}`, {
headers: {
"Content-Type": "application/json",
},
...options,
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
}
CRUD操作の実装
// READ: クライアント一覧を取得
async function getClients(): Promise<Client[]> {
const result = await apiRequest<Client[]>("/clients");
return result.data;
}
// READ: 特定のクライアントを取得
async function getClient(id: number): Promise<Client> {
const result = await apiRequest<Client>(`/clients/${id}`);
return result.data;
}
// CREATE: 新規クライアント登録
async function createClient(
client: Omit<Client, "id" | "createdAt">
): Promise<Client> {
const result = await apiRequest<Client>("/clients", {
method: "POST",
body: JSON.stringify(client),
});
return result.data;
}
// UPDATE: クライアント情報更新
async function updateClient(
id: number,
updates: Partial<Client>
): Promise<Client> {
const result = await apiRequest<Client>(`/clients/${id}`, {
method: "PUT",
body: JSON.stringify(updates),
});
return result.data;
}
// DELETE: クライアント削除
async function deleteClient(id: number): Promise<void> {
await apiRequest<void>(`/clients/${id}`, {
method: "DELETE",
});
}
ダッシュボードでの使用例
// dashboard.ts — ダッシュボードの表示ロジック
async function renderDashboard() {
try {
// クライアント一覧を取得
const clients = await getClients();
// アクティブなクライアントだけ表示
const activeClients = clients.filter(c => c.isActive);
// HTML生成
const container = document.getElementById("client-list");
if (!container) return;
container.innerHTML = activeClients
.map(client => `
<div class="client-card" data-id="${client.id}">
<h3>${client.name}</h3>
<p>${client.category} | ${client.owner}</p>
<p>月間アクセス: ${client.monthlyVisits.toLocaleString()}</p>
<button onclick="handleEdit(${client.id})">編集</button>
<button onclick="handleDelete(${client.id})">削除</button>
</div>
`)
.join("");
} catch (error) {
console.error("ダッシュボードの読み込みに失敗:", error);
showErrorMessage("データの取得に失敗しました。再度お試しください。");
}
}
// 新規クライアント追加フォームの処理
async function handleCreateClient(form: HTMLFormElement) {
const formData = new FormData(form);
const newClient = {
name: formData.get("name") as string,
owner: formData.get("owner") as string,
category: formData.get("category") as Client["category"],
monthlyVisits: 0,
isActive: true,
};
try {
await createClient(newClient);
await renderDashboard(); // 一覧を再描画
form.reset();
} catch (error) {
console.error("クライアント登録に失敗:", error);
}
}
エラーハンドリングのパターン
async function safeApiCall<T>(
apiCall: () => Promise<T>,
fallback: T
): Promise<T> {
try {
return await apiCall();
} catch (error) {
if (error instanceof Error) {
console.error(`API Error: ${error.message}`);
}
return fallback;
}
}
// 使用例: エラー時は空配列を返す
const clients = await safeApiCall(
() => getClients(),
[]
);
ポイント: API呼び出しは必ず失敗する可能性がある(ネットワーク障害、サーバーエラーなど)。try/catchで適切にエラーを処理し、ユーザーにフィードバックを返すことが重要です。
ShimaLinkのAPI設計が完成した。各クライアントのデータにはエンドポイントを通じてのみアクセスでき、不用意なデータ破壊のリスクが大幅に減った。
GET /api/clients → クライアント一覧
GET /api/clients/:id → 特定のクライアント
POST /api/clients → 新規クライアント登録
PUT /api/clients/:id → クライアント情報更新
DELETE /api/clients/:id → クライアント削除あなた: 「おお、フロントエンドから直接JSONファイルをいじらなくていいのか。安心感が違う!」
Mika: 「そういえば、予約データって毎回入力し直してますよね?前に入れたデータが消えちゃうことがあるんですけど…」
あなた: 「確かに。サーバーを再起動するとデータがリセットされますよね。」
Yuki: 「いいところに気づいたね。今のAPIは、データをメモリ上に持っているだけ。サーバーが止まればデータは消える。これを永続化するには——」
あなた: 「データベース!」
Yuki: 「正解。データを安全に保管し、高速に検索し、確実に取り出せる”住処”が必要だ。」
あなた: 「データベースって、SQLとかですよね?」
Yuki: 「次のステップはまさにそこ。APIの裏側にデータベースを接続して、データを永続化する方法を学ぼう。」
次のチャプター: Chapter 9: データの住処 — サーバー再起動でデータが消える問題を解決。データベースとSQLの基礎を学ぶ。
🧠 理解度チェック
Q1.REST APIにおいて、新しいデータを作成するHTTPメソッドは?
💡 ShimaLinkに新しいクライアントを登録するとき、POST /api/clientsを使ったのを思い出そう。
Q2.JSONで正しい形式はどれ?
💡 APIのリクエストボディやレスポンスはすべてJSON形式。ルールを間違えるとパースエラーになるよ。
Q3.HTTPステータスコード201の意味は?
💡 新規クライアントの登録が成功すると、サーバーは201ステータスと作成されたデータを返す。
Q4.RESTful APIのURL設計として適切なのはどれ?
💡 Yukiが「URLは名詞、HTTPメソッドが動詞」と教えてくれた設計原則だ。
Q5.fetchでPOSTリクエストを送る際、Content-Typeヘッダーに設定する値は?
💡 createClient関数でfetchのheadersに設定した値を思い出そう。
Q6.JSON.stringify()の役割は?
💡 fetchのbodyにはオブジェクトをそのまま渡せない。JSON.stringify()で文字列に変換してから送信する。
Q7.APIの「冪等性(べきとうせい)」とは何?
💡 PUTで同じデータを2回送っても同じ結果だけど、POSTを2回送ると2つのクライアントが作られてしまう。
Q8.APIのエラーレスポンスとして、クライアント側(リクエスト側)のミスを示すステータスコードの範囲は?
💡 存在しないクライアントIDを指定すると404、不正な形式のデータを送ると400が返ってくるよ。
❓ よくある質問
GETとPOSTの使い分けが分からない
**シンプルなルール**: データを取得するだけならGET、データを送信して何かを作成するならPOST。 ``` GET /api/clients → 「クライアント一覧をください」 POST /api/clients → 「新しいクライアントを登録してください」 ``` | | GET | POST | |---|---|---| | 目的 | データ取得 | データ作成 | | ボディ | なし | あり(送信データ) | | ブラウザ履歴 | 残る | 残らない | | ブックマーク | 可能 | 不可 | | 冪等性 | あり | なし |
PUTとPATCHの違いは?
どちらもデータの更新に使いますが、範囲が異なります。 - **PUT**: リソースの**全体**を置き換える - **PATCH**: リソースの**一部**だけを更新する ```typescript // PUT: 全プロパティを送る必要がある await fetch("/api/clients/1", { method: "PUT", body: JSON.stringify({ name: "海風テラス", category: "カフェ", visits: 1500, isActive: true }) }); // PATCH: 変更したい部分だけ送ればOK await fetch("/api/clients/1", { method: "PATCH", body: JSON.stringify({ visits: 1500 }) }); ``` 実務ではPATCHの方が便利な場面が多いです。
APIリクエストで「404 Not Found」が返ってくる
URLが正しいか確認しましょう。 **チェックリスト**: 1. **URLのスペルミス**: `/api/clents` → `/api/clients` 2. **IDが存在するか**: `/api/clients/999` でID 999が存在しない 3. **ベースURLの確認**: `http://localhost:3000` vs `http://localhost:8080` 4. **末尾のスラッシュ**: `/api/clients/` vs `/api/clients` ```typescript // デバッグ: 実際のURLを確認 const url = "/api/clients"; console.log("リクエストURL:", url); const response = await fetch(url); console.log("ステータス:", response.status); ```
JSON.parse()でエラーが出る
**JSONの形式が不正**な可能性が高いです。 **よくある原因**: ```javascript // NG: シングルクォート JSON.parse("{'name': 'Mika'}"); // Error! // OK: ダブルクォート JSON.parse('{"name": "Mika"}'); // OK // NG: 末尾カンマ JSON.parse('{"name": "Mika",}'); // Error! // NG: 空文字列 JSON.parse(""); // Error! ``` **デバッグ方法**: ```javascript try { const data = JSON.parse(responseText); } catch (e) { console.error("JSONパースエラー:", e.message); console.log("受信データ:", responseText); } ```
fetchでPOSTしたのにサーバーにデータが届かない
**Content-Typeヘッダーの設定忘れ**が最も多い原因です。 ```typescript // NG: headersがない await fetch("/api/clients", { method: "POST", body: JSON.stringify({ name: "テスト" }) }); // サーバーがJSONとして認識しない // OK: Content-Typeを設定 await fetch("/api/clients", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "テスト" }) }); ``` **その他のチェックポイント**: - `JSON.stringify()`で文字列に変換しているか - サーバー側でリクエストボディをパースしているか - ネットワークタブでリクエスト内容を確認
APIのURLをどう設計すればいいか分からない
**RESTful URLの基本ルール**: 1. **名詞・複数形を使う**: `/api/clients`(`/api/getClient`ではない) 2. **階層でリソースの関係を表す**: `/api/clients/1/reservations` 3. **動詞はHTTPメソッドで表す**: GET/POST/PUT/DELETE ``` 良い例: GET /api/clients 全クライアント GET /api/clients/1 ID=1のクライアント POST /api/clients クライアント作成 GET /api/clients/1/reservations クライアント1の予約 悪い例: /api/getAllClients /api/createNewClient /api/deleteClient?id=1 ```
response.json()とJSON.parse()の違いは?
**結果は同じ**ですが、使いどころが違います。 ```typescript // response.json() — fetchのレスポンスをパース(推奨) const response = await fetch("/api/clients"); const data = await response.json(); // Promiseを返す // JSON.parse() — 任意の文字列をパース const jsonString = '{"name": "Mika"}'; const obj = JSON.parse(jsonString); // 同期処理 ``` **response.json()の方が便利な理由**: - ストリームからの読み取りを自動処理 - Promiseベースで非同期対応 - fetchのレスポンスに最適化されている **JSON.parse()を使う場面**: - localStorage のデータを読む - WebSocket のメッセージをパース - 手動でJSON文字列を扱うとき
APIテストはどうすればいい?
**ブラウザの開発者ツール**や**専用ツール**を使います。 **方法1: ブラウザの開発者ツール(Network タブ)** - F12で開いて Network タブを選択 - APIリクエストの内容やレスポンスを確認 **方法2: curlコマンド** ```bash # GET curl http://localhost:3000/api/clients # POST curl -X POST http://localhost:3000/api/clients \ -H "Content-Type: application/json" \ -d '{"name": "テスト"}' ``` **方法3: VS Code拡張機能** - REST Client(.httpファイルでリクエストを管理) - Thunder Client(GUIでリクエスト送信)
ステータスコード200と201の使い分けは?
**200 OK**: 一般的な成功。GET、PUT、PATCHなどで使います。 **201 Created**: 新しいリソースが作成された。POSTで使います。 ``` GET /api/clients → 200 OK(データ取得成功) POST /api/clients → 201 Created(新規作成成功) PUT /api/clients/1 → 200 OK(更新成功) DELETE /api/clients/1 → 204 No Content(削除成功、返すデータなし) ``` **204 No Content**はDELETEの成功時によく使います。レスポンスボディが空であることを示します。