JavaScript | 非同期処理:fetch / API 通信 – API 設計との関係

JavaScript JavaScript
スポンサーリンク

API 設計と fetch の関係の全体像

fetch 自体は「URL に HTTP リクエストを送って、レスポンスを受け取るだけ」の素朴な道具です。
でも、実際のコードの書きやすさ・分かりやすさは、API の設計が良いかどうかにかなり左右されます。

同じ fetch でも、API がこうだと楽です。

  • わかりやすい URL と HTTP メソッド
  • 一貫した JSON 形式
  • 分かりやすいステータスコードとエラー形式
  • 認証方法がシンプル

逆に、API がバラバラなルールで設計されていると、フロント側では if 文と例外処理だらけになり、初心者が見ても「何をしているのか」が全然分からなくなります。

ここが重要です。
fetch の書き方だけを覚えるのではなく、「どんな API 設計だと、fetch をシンプルに書けるのか?」という視点を持つと、一気に理解が深くなります。


エンドポイント設計と fetch の書きやすさ

リソースと URL 設計

API はよく「ユーザー」「記事」「商品」のような「リソース」単位で考えます。

例として「ユーザー一覧を取得する API」を考えます。

良い設計の例は、だいたいこんな感じです。

GET /api/users

これだと、フロント側の fetch はとても素直に書けます。

const response = await fetch("/api/users");
const users = await response.json();
JavaScript

ユーザー詳細なら、

GET /api/users/123

で、

const response = await fetch("/api/users/123");
const user = await response.json();
JavaScript

このように「URLの形から、何をしているかが想像できる」API は、fetch のコードを見たときにも意味がすぐ分かります。

もしこうだったら、どうでしょう。

POST /api/getUserList
POST /api/fetchUserDetailById

リソースではなく、「操作名っぽいもの」が乱立していると、フロント側のコードも読みにくくなります。

await fetch("/api/getUserList", { method: "POST" });
await fetch("/api/fetchUserDetailById", { method: "POST", body: ... });
JavaScript

「これは何をしている URL なんだっけ?」と毎回読み解かないといけません。

ここが重要です。
URL とメソッドの設計(API 設計)が素直だと、fetch を書くときに「意図」がそのままコードに現れるようになります。
逆に API が分かりにくいと、fetch をいくら頑張ってもコードが意味不明になりがちです。

HTTP メソッドと意味の対応

良い API 設計では、HTTP メソッドの意味が統一されています。

例えばユーザーに対して:

  • GET /api/users … 一覧取得
  • GET /api/users/123 … 1件取得
  • POST /api/users … 新規作成
  • PUT /api/users/123 … 全体更新
  • PATCH /api/users/123 … 部分更新
  • DELETE /api/users/123 … 削除

こう決まっていると、フロント側の fetch もパターン化できます。

// 一覧
await fetch("/api/users");

// 作成
await fetch("/api/users", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(newUser),
});

// 削除
await fetch("/api/users/123", { method: "DELETE" });
JavaScript

ここが重要です。
API 設計で HTTP メソッドの意味が揃っていれば、
「メソッドを見ただけで何をしたいか分かる fetch」が書けます。

これは、後から自分や他人がコードを読むときの理解コストを大きく下げてくれます。


レスポンス設計(JSON とステータスコード)と fetch

一貫した JSON 形式は「フロントの救い」

API から返ってくる JSON の形式がバラバラだと、
フロントのコードは if / try-catch だらけになります。

良い例として、「成功時はこう」「エラー時はこう」と形を揃えているケースを考えます。

例えば、成功時は必ずこんな形だとします。

{
  "success": true,
  "data": { "id": 1, "name": "太郎" }
}

エラー時はこう。

{
  "success": false,
  "error": { "code": "VALIDATION_ERROR", "message": "名前は必須です" }
}

この前提があれば、フロント側はいつも同じパターンで処理できます。

const response = await fetch("/api/user/1");

if (!response.ok) {
  throw new Error("HTTP エラー: " + response.status);
}

const body = await response.json();

if (!body.success) {
  console.error("アプリケーションエラー:", body.error.message);
  return;
}

console.log("ユーザー:", body.data);
JavaScript

API 設計がこう決めてくれていれば、
どのエンドポイントでも同じように書けるので、初心者でもパターンを覚えやすくなります。

ステータスコードと fetch の関係

API 設計側で、ステータスコードの使い方も揃えておくとさらに楽になります。

例えば:

  • 成功時は 200 / 201 など成功系のステータス
  • パラメータ不正のときは 400
  • 認証エラーは 401
  • 権限がないときは 403
  • その ID のデータがないときは 404
  • サーバー側のバグや不調は 500

と統一されていると、フロント側はこう書けます。

const response = await fetch("/api/profile");

if (response.status === 401) {
  // ログイン画面へ誘導
} else if (response.status === 404) {
  // 「プロフィールがまだ設定されていません」と表示
} else if (!response.ok) {
  // その他のエラー(500など)
}
JavaScript

ここが重要です。
API 設計でステータスコードの意味付けがちゃんとしていると、
fetch 側は「status / ok を見るだけで、どう振る舞うか決められる」ようになります。
逆に API が 200 しか返さず、エラーを全部 JSON の中の謎フラグで表現すると、フロント側が苦しみます。


認証・認可の設計と fetch

認証のやり方が統一されているとヘッダーがシンプルになる

API によっては、「このヘッダーでトークンを送ってください」と決められています。
代表的なのが Authorization ヘッダーです。

例えば API 設計が、

「全ての保護されたエンドポイントは、
Authorization: Bearer <token> を受け取る」

と決めてくれると、フロント側はパターン化できます。

async function fetchWithAuth(url, token, options = {}) {
  const headers = {
    ...(options.headers || {}),
    Authorization: `Bearer ${token}`,
  };

  const response = await fetch(url, {
    ...options,
    headers,
  });

  return response;
}
JavaScript

どの API でも、これを使えば OK です。

API 設計がバラバラで、

  • あるエンドポイントは X-Auth-Token ヘッダー
  • 別のエンドポイントはクエリパラメータ ?token=
  • さらに別のは body の中に token フィールド

みたいなことになっていると、フロント側は毎回書き分けないといけません。

認証失敗時のルールが揃っていると、処理も揃う

API 側が「認証エラーは必ず 401」を返すと決めてくれれば、
フロント側はどの fetch でも同じように処理できます。

const response = await fetchWithAuth("/api/profile", token);

if (response.status === 401) {
  redirectToLogin();
  return;
}
JavaScript

設計が良くないと、「このエンドポイントは 403 を返す」「こっちは 200 で body.errorCode を見る」など、エンドポイントごとに条件分岐が増えます。

ここが重要です。
認証・認可の設計が揃っている API だと、
「認証付き fetch」を 1 つ用意するだけで、アプリ全体をカバーできます。
これはフロントエンド実装にとって、ものすごく大きな恩恵です。


ページネーションや検索など、クエリ設計と fetch

ページングの API 設計とフロントコード

ユーザー一覧をページングで取る API を考えてみます。

API 設計がこんな感じだとします。

GET /api/users?page=2&limit=10

レスポンスはこう。

{
  "data": [
    { "id": 11, "name": "ユーザー11" },
    { "id": 12, "name": "ユーザー12" }
  ],
  "page": 2,
  "limit": 10,
  "total": 47
}

このルールが決まっていれば、フロント側はシンプルに書けます。

async function loadUsers(page = 1, limit = 10) {
  const params = new URLSearchParams({ page, limit });
  const response = await fetch(`/api/users?${params.toString()}`);

  if (!response.ok) {
    throw new Error("HTTP エラー: " + response.status);
  }

  const body = await response.json();
  return body;
}
JavaScript

API 設計が
「あるエンドポイントは page / limit
別のエンドポイントは offset / size
とバラバラだと、その分だけ fetch の書き分けが必要になり、コードが散らかります。

検索条件の設計と fetch の見通し

例えば、ユーザー検索 API を考えます。

API 設計で、「検索条件はすべてクエリパラメータで受け取る」と決めていれば、

GET /api/users?keyword=taro&role=admin

という形で呼び出せます。

fetch 側も素直です。

const params = new URLSearchParams({
  keyword: "taro",
  role: "admin",
});

const response = await fetch(`/api/users?${params.toString()}`);
JavaScript

もし API 設計が、

  • この検索は POST + JSON
  • あの検索は GET + クエリ
  • 別の検索はなぜかパス /api/users/search/admin/taro

などバラバラだと、フロント側が全部覚えておかないといけません。

ここが重要です。
API 側で「検索条件はこう表現する」「ページングはこう」と揃えてくれると、
fetch のコードもパターン化されて、初心者でも見通しがよくなります。


小さな「良い API 設計」と fetch コードのセットの例

API 側の設計(想定)

ユーザー関係の API を、こう設計したと仮定します。

ユーザー一覧取得
GET /api/users?page=1&limit=10

ユーザー詳細取得
GET /api/users/:id

ユーザー作成
POST /api/users
ボディ JSON、成功時 201 Created、ボディは { success: true, data: {...} }

エラー時(共通)
HTTP ステータスは 4xx / 5xx、ボディはこう。

{
  "success": false,
  "error": {
    "code": "USER_NOT_FOUND",
    "message": "ユーザーが見つかりません"
  }
}

それに対応するフロント側の fetch ヘルパー

こういう API 設計なら、フロント側は共通のヘルパーを作れます。

async function callApi(url, options = {}) {
  const response = await fetch(url, {
    headers: {
      "Content-Type": "application/json",
      Accept: "application/json",
      ...(options.headers || {}),
    },
    ...options,
  });

  const body = await response.json().catch(() => null);

  if (!response.ok || (body && body.success === false)) {
    const message =
      body?.error?.message ||
      `HTTP エラー: ${response.status}`;
    throw new Error(message);
  }

  return body;
}
JavaScript

これがあれば、各画面のコードはかなり短くできます。

ユーザー一覧:

async function loadUsers(page = 1, limit = 10) {
  const params = new URLSearchParams({ page, limit });
  const body = await callApi(`/api/users?${params.toString()}`);
  return body.data;
}
JavaScript

ユーザー詳細:

async function loadUser(id) {
  const body = await callApi(`/api/users/${id}`);
  return body.data;
}
JavaScript

ユーザー作成:

async function createUser(user) {
  const body = await callApi("/api/users", {
    method: "POST",
    body: JSON.stringify(user),
  });
  return body.data;
}
JavaScript

ここが重要です。
API 設計が「URL・メソッド・JSON形式・エラー形式」を揃えてくれていると、
フロント側は共通の fetch ラッパーを 1 個作るだけで、ほぼ全部の通信パターンをカバーできます。
これは、初心者にとっても「覚えるべき形が少なくて済む」という意味で大きなメリットです。


初心者として「API 設計との関係」で本当に押さえてほしいこと

fetch の書き方だけを見ても、実際のアプリではすぐ限界がきます。
なぜなら、fetch は「API 設計の良し悪し」をそのまま受け止める鏡だからです。

URL と HTTP メソッドがリソース指向で分かりやすく設計されていると、
fetch のコードも「見ただけで意図が分かる」ものになります。

レスポンスの JSON 形式やステータスコードの使い方が統一されていると、
「fetch → ok チェック → json → data / error を見る」パターンだけで、ほとんどの通信を扱えるようになります。

認証・ページング・検索など API のルールが揃っていると、
フロント側は共通のヘルパー関数を作りやすくなり、
画面ごとのコードは「何をしたいか」だけに集中できるようになります。

ここが重要です。
fetch の練習をするとき、
「この API の設計は、フロントから見て分かりやすいか?」
「自分なら、レスポンスやエラーをどういう形に設計するか?」
という視点も一緒に持ってみてください。
“ただ叩くだけの人” から、“会話の相手(API)の設計も意識できる人” に変わると、
コードの質も、チームで話せる内容も、一段レベルアップします。

タイトルとURLをコピーしました