JavaScript | 非同期処理:設計・理解の深化 - 非同期 API 設計

JavaScript JavaScript
スポンサーリンク

「非同期 API 設計」って何を考えること?

まず前提から整理します。
ここでいう「非同期 API」は、サーバーの REST API というより、
「あなたが JavaScript で提供する非同期関数の“顔つき”」のことです。

例えば、ライブラリ側に

async function fetchUser(id) { ... }
JavaScript

という関数を用意して、
アプリ側から

const user = await fetchUser(1);
JavaScript

と呼んでもらう。
この「呼ばれ方」まで含めて設計するのが、非同期 API 設計です。

重要なのは、
「中で何をしているか」よりも先に、
「外から見てどう振る舞うか」を決めることです。


非同期 API の基本形を押さえる

Promise を返す、という約束

非同期 API の一番の約束は、「Promise を返すこと」です。

function fetchUser(id) {
  return fetch(`/api/users/${id}`).then((res) => res.json());
}
JavaScript

この関数は async を付けていませんが、
戻り値が Promise なので、呼び出し側は

const user = await fetchUser(1);
JavaScript

と書けます。

async を付けると、もっと素直に書けます。

async function fetchUser(id) {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) {
    throw new Error("ユーザー取得に失敗しました");
  }
  return res.json();
}
JavaScript

ここで大事なのは、
「非同期 API は“Promise を返す関数”として定義する」
という意識です。

内部で何を await しているかは、呼び出し側には関係ありません。
呼び出し側にとって大事なのは、
「この関数を呼ぶと、いつか結果かエラーが返ってくる Promise が手に入る」
という約束だけです。

戻り値の形をシンプルにする

非同期 API の戻り値は、できるだけ「一つの意味」に絞ると扱いやすくなります。

例えば、こういうのはあまり良くありません。

async function fetchUserAndPosts(id) {
  const userRes = await fetch(`/api/users/${id}`);
  const postsRes = await fetch(`/api/posts?userId=${id}`);
  return {
    userRes,
    postsRes,
  };
}
JavaScript

呼び出し側は、さらに json を呼ばないといけません。

const { userRes, postsRes } = await fetchUserAndPosts(1);
const user = await userRes.json();
const posts = await postsRes.json();
JavaScript

非同期 API の設計としては、
「呼び出し側がすぐ使える形」で返してあげる方が親切です。

async function fetchUserAndPosts(id) {
  const [userRes, postsRes] = await Promise.all([
    fetch(`/api/users/${id}`),
    fetch(`/api/posts?userId=${id}`),
  ]);

  if (!userRes.ok || !postsRes.ok) {
    throw new Error("ユーザーまたは投稿の取得に失敗しました");
  }

  const [user, posts] = await Promise.all([
    userRes.json(),
    postsRes.json(),
  ]);

  return { user, posts };
}
JavaScript

これなら、呼び出し側は

const { user, posts } = await fetchUserAndPosts(1);
JavaScript

と書くだけで済みます。

ここが重要です。
「非同期 API の戻り値は、“そのまま使える形”にして返す。
呼び出し側に余計な await や判定を押し付けない。」


エラーの扱いを API の一部として設計する

失敗したとき、どう振る舞うかを決める

非同期 API は、成功だけでなく失敗も必ず起こります。
ネットワークエラー、タイムアウト、サーバーエラーなどです。

設計として決めるべきなのは、
「失敗したときに何をするか」です。

典型的なパターンは二つです。

一つ目は、「失敗したら例外を投げる」パターン。

async function fetchUser(id) {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) {
    throw new Error(`ユーザー取得に失敗しました: ${res.status}`);
  }
  return res.json();
}
JavaScript

呼び出し側は、try/catch で扱います。

try {
  const user = await fetchUser(1);
  renderUser(user);
} catch (err) {
  showErrorMessage(err.message);
}
JavaScript

二つ目は、「成功・失敗をオブジェクトで返す」パターン。

async function fetchUserSafe(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) {
      return { ok: false, error: new Error("ユーザー取得に失敗しました") };
    }
    const user = await res.json();
    return { ok: true, user };
  } catch (err) {
    return { ok: false, error: err };
  }
}
JavaScript

呼び出し側は、戻り値を見て分岐します。

const result = await fetchUserSafe(1);
if (!result.ok) {
  showErrorMessage(result.error.message);
} else {
  renderUser(result.user);
}
JavaScript

どちらが正解というより、
「この API はどちらのスタイルで振る舞うか」を決めて、
一貫させることが大事です。

ここが重要です。
「エラーの扱い方は、実装の詳細ではなく“API の仕様”である。
“投げるのか、返すのか”を決めて、呼び出し側が迷わないようにする。」


オプションや設定をどう渡すか

引数が増えたときはオブジェクトにまとめる

非同期 API は、だんだん機能が増えていきます。
タイムアウトを設定したい、
リトライ回数を指定したい、
キャッシュを使うかどうか切り替えたい、など。

引数を素直に増やしていくと、すぐにこうなります。

async function fetchUser(id, useCache, timeoutMs, retryCount) { ... }
JavaScript

呼び出し側は、何が何だか分からなくなります。

const user = await fetchUser(1, true, 5000, 3);
JavaScript

こういうときは、オブジェクトにまとめるのが定番です。

async function fetchUser(id, options = {}) {
  const {
    useCache = true,
    timeoutMs = 5000,
    retryCount = 0,
  } = options;

  // ここで options を使って実装
}
JavaScript

呼び出し側は、意図が読みやすくなります。

const user = await fetchUser(1, {
  useCache: false,
  timeoutMs: 3000,
});
JavaScript

ここでのポイントは、
「オプションは“名前付き”で渡せるようにする」ことです。
非同期 API は時間が絡むので、
「この数字は何ミリ秒?何回?」といった混乱を避けるためにも、
オブジェクト引数はかなり効きます。


キャンセルや中断をどう扱うか(少し先の話)

AbortController を使ったキャンセル可能な API

少しレベルを上げると、
「非同期処理を途中でやめたい」というニーズが出てきます。

例えば、検索ボックスに文字を打つたびに API を叩くとき、
前のリクエストはキャンセルしたい、などです。

そのときに出てくるのが AbortController です。

キャンセル可能な非同期 API を設計するなら、
「AbortSignal を受け取る」形にしておくと拡張しやすくなります。

async function fetchUser(id, { signal } = {}) {
  const res = await fetch(`/api/users/${id}`, { signal });
  if (!res.ok) {
    throw new Error("ユーザー取得に失敗しました");
  }
  return res.json();
}
JavaScript

呼び出し側は、こう書けます。

const controller = new AbortController();

const promise = fetchUser(1, { signal: controller.signal });

// 途中でやめたくなったら
controller.abort();
JavaScript

初心者の段階では、
「キャンセル可能な API という設計もある」
くらいを知っておけば十分です。

大事なのは、
「将来キャンセルを入れたくなったときに、引数の設計が邪魔をしない形にしておく」
という視点です。


「非同期 API 設計」を意識したミニ例題

ごちゃっとした関数を、API として整える

まず、よくある「とりあえず動く」コードから。

async function init() {
  const res = await fetch("/api/user");
  if (!res.ok) {
    alert("ユーザー取得に失敗しました");
    return;
  }
  const user = await res.json();

  const postsRes = await fetch(`/api/posts?userId=${user.id}`);
  if (!postsRes.ok) {
    alert("投稿取得に失敗しました");
    return;
  }
  const posts = await postsRes.json();

  renderUser(user);
  renderPosts(posts);
}
JavaScript

これは「ページ初期化の処理」と「API 呼び出しの詳細」が混ざっています。
これを「非同期 API」と「アプリ側」に分けてみます。

まず、API 側。

async function fetchCurrentUser() {
  const res = await fetch("/api/user");
  if (!res.ok) {
    throw new Error("ユーザー取得に失敗しました");
  }
  return res.json();
}

async function fetchUserPosts(userId) {
  const res = await fetch(`/api/posts?userId=${userId}`);
  if (!res.ok) {
    throw new Error("投稿取得に失敗しました");
  }
  return res.json();
}

async function fetchUserPageData() {
  const user = await fetchCurrentUser();
  const posts = await fetchUserPosts(user.id);
  return { user, posts };
}
JavaScript

これが「非同期 API」の側です。
アプリ側は、こうなります。

async function initUserPage() {
  try {
    const { user, posts } = await fetchUserPageData();
    renderUser(user);
    renderPosts(posts);
  } catch (err) {
    showErrorMessage(err.message);
  }
}
JavaScript

ここまで分けると、

API 側は「データをどう取るか」を担当
アプリ側は「データをどう見せるか」を担当

という構造がはっきりします。

ここが重要です。
「非同期 API 設計とは、“アプリ側から見たときに気持ちよく使える非同期関数の形を決めること”。
実装の前に、“どう呼ばれたいか”を先に考える。」


初心者として「非同期 API 設計」で意識してほしいこと

最後に、あなたに持っていてほしい問いをまとめます。

この関数は、Promise を返す“非同期 API”として一貫しているか?
戻り値は、そのまま使える形になっているか?
失敗したときの振る舞い(投げるか、返すか)は決めてあるか?
引数が増えたときに、呼び出し側が読みやすい形になっているか?
この関数を使う側のコードを想像したとき、「気持ちよく await できる」形になっているか?

おすすめの練習は、
自分の非同期関数を一つ選んで、
「これはライブラリの API だ」と仮定して、
呼び出し側のコードを先に書いてみることです。

例えば、

const { user, posts } = await fetchUserPageData(1);
JavaScript

と書けたら気持ちいいな、と思ったら、
その形を実現するように中身を設計していく。

そうやって
「どう呼ばれたいか」から逆算して非同期関数を設計し始めたとき、
あなたはもう「async/await を知っている人」から、
「非同期 API を設計できるエンジニア」 に一歩踏み込んでいます。

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