08
📈 成長編 Chapter 08

APIという架け橋

フロントエンドとバックエンドをつなぐ

約55分
REST API · intro JSON · intro HTTP Methods · intro API Design · intro
目次(27セクション)
🎬 Story — Introduction

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 APIURL + 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説明リクエスト例
GETReadデータの取得GET /api/clients
POSTCreate新規データの作成POST /api/clients
PUTUpdateデータの更新(全体)PUT /api/clients/1
DELETEDeleteデータの削除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("削除成功");
}

ステータスコード

コード意味使いどころ
200OKGET/PUT成功
201CreatedPOST成功
204No ContentDELETE成功
400Bad Requestリクエスト不正
404Not Foundリソースが見つからない
500Internal Server Errorサーバー側エラー

メソッドの特性

特性GETPOSTPUTDELETE
ボディありなしありありなし
冪等性ありなしありあり
安全はいいいえいいえいいえ

冪等(べきとう)」とは、同じリクエストを何度送っても結果が同じという意味。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();

統一されたレスポンス形式を定義しよう。

// 成功レスポンス
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 エンドポイント一覧
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
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で適切にエラーを処理し、ユーザーにフィードバックを返すことが重要です。

📖 Story — Conclusion

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の成功時によく使います。レスポンスボディが空であることを示します。