mcpField
F
frontend-api-integration-patterns

frontend-api-integration-patterns

github.com/sickn33開発48.7k85 回インストール今日更新
リポジトリ

frontend-api-integration-patterns について

フロントエンド開発者向けの API 連携スキルです。非同期状態、リクエストのキャンセルや再試行、重複排除を扱い、古いデータや重複呼び出しを防ぎます。

主な機能

  1. 01API 層の集約とエラーの正規化
  2. 02非同期状態の競合防止
  3. 03AbortController によるリクエストキャンセル
  4. 04指数バックオフによる選択的な再試行
  5. 05デバウンスとリクエスト重複排除

活用シーン

  1. 01古いレスポンスによるデータ上書きの防止
  2. 02検索入力による API 呼び出しの削減
  3. 03複数コンポーネント間の重複リクエスト防止
  4. 04キャンセル可能な ML 予測リクエストの処理

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/sickn33/agentic-awesome-skills --skill frontend-api-integration-patterns

インストール後、Claude Code で/frontend-api-integration-patternsを入力して読み込まれたか確認します。

詳細

フロントエンド API 統合パターン

概要

このスキルでは、フロントエンドアプリケーションをバックエンド API と統合するための、本番環境で使えるパターンを紹介します。

フロントエンドで起きる問題の多くは、API の呼び出しが難しいことではなく、非同期動作の処理が不適切であることが原因です。その結果、競合状態や古いデータ、リクエストの重複、ユーザーエクスペリエンスの低下につながります。

このスキルでは、API 呼び出しを機能させることだけでなく、正確性、堅牢性、ユーザーエクスペリエンスを重視します。


このスキルを使う場面

  • フロントエンドアプリ(React、React Native、Vue など)をバックエンド API に接続するとき
  • ML/AI エンドポイント(/predict、/recommend)を統合するとき
  • UI で非同期データを処理するとき
  • 古いデータ、UI のちらつき、リクエストの重複を解消するとき
  • 拡張性の高いフロントエンド API レイヤーを設計するとき

基本パターン

1. API レイヤー(関心の分離)

API ロジックを一元化し、エラーを正規化します。

export class ApiError extends Error {
  constructor(message, status, payload = null) {
    super(message);
    this.name = "ApiError";
    this.status = status;
    this.payload = payload;
  }
}

export const apiClient = async (url, options = {}) => {
  const res = await fetch(url, {
    headers: { "Content-Type": "application/json" },
    ...options,
  });

  if (!res.ok) {
    let payload = null;
    try {
      payload = await res.json();
    } catch (_) {}

    throw new ApiError(
      payload?.message || "Request failed",
      res.status,
      payload
    );
  }

  // handle empty responses safely (e.g. 204 No Content)
  if (res.status === 204) return null;

  const text = await res.text();
  return text ? JSON.parse(text) : null;
};

2. 競合状態に安全な状態管理

古いレスポンスが新しいデータを上書きしないようにします。

useEffect(() => {
  let cancelled = false;

  const load = async () => {
    try {
      setLoading(true);
      setError(null);

      const result = await getUser();

      if (!cancelled) setData(result);
    } catch (err) {
      if (!cancelled) setError(err.message);
    } finally {
      if (!cancelled) setLoading(false);
    }
  };

  load();

  return () => {
    cancelled = true;
  };
}, []);

fetch 以外の非同期処理には、キャンセルフラグを使用してください。ネットワークリクエストには、AbortController を優先してください。


3. リクエストのキャンセル(AbortController)

実行中のリクエストをキャンセルして、メモリーリークや古い状態への更新を防ぎます。

useEffect(() => {
  const controller = new AbortController();

  const load = async () => {
    try {
      const data = await getUser({ signal: controller.signal });
      setData(data);
    } catch (err) {
      if (err.name === "AbortError") return;
      setError(err.message);
    }
  };

  load();
  return () => controller.abort();
}, [userId]);

4. 指数バックオフによる再試行

明示的に再実行しても安全な操作と、分類済みの一時的な障害に限って再試行してください。デフォルトでは、試行は 1 回です。任意のプログラミングエラーや解析例外をネットワークエラーとして分類しないでください。実際のアダプターでは、プロバイダーの Retry-After と全体の期限を尊重してください。また、レート制限のレスポンスに対して、この単純なバックオフを有効にしないでください。POST による予測の例では、バックエンドの契約によって再実行の安全性が独立して保証されない限り、意図的に再試行を有効にしていません。

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

const fetchWithBackoff = async (fn, retries = 3, delay = 300, replaySafe = false) => {
  try {
    return await fn();
  } catch (err) {
    const isAbort = err.name === "AbortError";
    const isHttpError = typeof err.status === "number";
    const isNetworkFailure = err.code === "NETWORK_ERROR";
    const isRetryable = replaySafe && !isAbort &&
      (isNetworkFailure || (isHttpError && err.status >= 500));

    if (retries <= 0 || !isRetryable) throw err;

    const nextDelay = delay * 2 + Math.random() * 100;
    await sleep(nextDelay);

    return fetchWithBackoff(fn, retries - 1, nextDelay, replaySafe);
  }
};

5. API 呼び出しのデバウンス

過剰な API 呼び出し(検索入力など)を避けます。

const useDebounce = (value, delay = 400) => {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const t = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(t);
  }, [value, delay]);

  return debounced;
};

6. リクエストの重複排除

コンポーネント間での API 呼び出しの重複を防ぎます。

const inFlight = new Map();

export const dedupedFetch = (key, fn) => {
  if (inFlight.has(key)) return inFlight.get(key);

  const promise = fn().finally(() => inFlight.delete(key));
  inFlight.set(key, promise);
  return promise;
};

例

例 1:キャンセル処理を備えた ML 予測
const controllerRef = useRef(null);

const handlePredict = async (input) => {
  controllerRef.current?.abort();
  const controller = new AbortController();
  controllerRef.current = controller;

  try {
    const result = await fetchWithBackoff(() =>
      apiClient("/predict", {
        method: "POST",
        body: JSON.stringify({ text: input }),
        signal: controller.signal,
      })
    );

    if (!controller.signal.aborted && controllerRef.current === controller) setOutput(result);
  } catch (err) {
    if (err.name === "AbortError") return;
    if (controllerRef.current === controller) setError(err.message);
  }
};

例 2:デバウンス付き検索
const debouncedQuery = useDebounce(query, 400);

useEffect(() => {
  if (!debouncedQuery) return;

  const controller = new AbortController();

  searchAPI(debouncedQuery, { signal: controller.signal })
    .then(setResults)
    .catch((err) => {
      if (err.name !== "AbortError") {
        setError("Search failed. Please try again.");
      }
    });

  return () => controller.abort();
}, [debouncedQuery]);

例 3:楽観的 UI 更新
const deleteItem = async (id) => {
  const previous = items;

  setItems((curr) => curr.filter((item) => item.id !== id));

  try {
    await apiClient(`/items/${id}`, { method: "DELETE" });
  } catch (err) {
    setItems(previous);
    setError("Delete failed. Please try again.");
  }
};

ベストプラクティス

  • ✅ 専用のレイヤーに API ロジックを集約する
  • ✅ カスタムエラークラスを使ってエラーを正規化する
  • ✅ ローディング、エラー、成功の各状態を必ず処理する
  • ✅ リクエストのキャンセルには AbortController を使用する
  • ✅ 一時的な障害(5xx)のみ再試行する
  • ✅ 入力に応じて呼び出す API にはデバウンスを使用する
  • ✅ 同一のリクエストを重複排除する

アンチパターン

  • ❌ 4xx エラーを再試行する
  • ❌ リクエストをキャンセルしない(メモリーリーク)
  • ❌ 競合状態が起きやすい状態更新
  • ❌ エラーを黙って無視する
  • ❌ 複数のリクエストでローディング/エラー状態をグローバルに共有する
  • ❌ コンポーネント内で API を繰り返し直接呼び出す

よくある問題

問題: UI に古いデータが表示される 解決策: キャンセルを使用するか、古いレスポンスを無視する

問題: 入力中に API 呼び出しが多すぎる 解決策: デバウンスとキャンセルを使用する

問題: 複数のコンポーネントからリクエストが重複する 解決策: リクエストの重複排除を使用する

問題: 再試行中にサーバーへ過剰な負荷がかかる 解決策: 指数バックオフを使用する

問題: コンポーネントのアンマウント後に状態が更新される 解決策: AbortController のクリーンアップを使用する


制限事項

  • これらの例ではバニラ JavaScript のパターンを使用しています。React Query、SWR、Apollo、Relay などのツールを使う場合は、使用しているフレームワークのデータ取得ライブラリーに合わせて調整してください。
  • バックエンドが冪等性キーなど、重複しても安全であることを保証する仕組みを提供していない限り、冪等でないミューテーションを再試行しないでください。
  • 特権を持つ API キーをフロントエンドコードで公開しないでください。機密性の高いリクエストはバックエンド経由でプロキシしてください。

追加リソース


コメント