Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

fetch APIの基本

JavaScriptでAPIリクエストを送るためのfetch APIについて学んでいきましょう。前回Thunder Clientで体験したAPIリクエストを、今度はコードで実装してみます。

この記事で学べること

  • fetch APIの基本構文と使い方
  • PromiseとAsync/Awaitの関係
  • GETリクエストの実装方法
  • POSTリクエストの実装方法
  • エラーハンドリングの基本パターン
  • AbortControllerによるキャンセル処理

fetch APIとは

fetchは、ブラウザに標準で備わっているAPIリクエスト用の関数です。XMLHttpRequest(XHR)の後継として登場し、現在のWeb開発ではこちらが主流です。Node.js 18以降でもネイティブサポートされているので、サーバーサイドでも同じ書き方で使えます(便利ですね)。

最小構成

fetch("https://jsonplaceholder.typicode.com/posts/1");

たったこれだけでHTTPリクエストが送れます。ただし、このままでは結果を受け取れません。fetchPromiseを返すので、適切に処理する必要があります。

Promiseとasync/await

fetchを使いこなすには、JavaScriptの非同期処理を理解しておく必要があります。

Promiseとは

Promiseは「将来の結果を約束するオブジェクト」です。APIリクエストのように時間がかかる処理の結果を扱うために使います。

// fetchはPromiseを返す
const promise = fetch("https://jsonplaceholder.typicode.com/posts/1");
console.log(promise); // Promise { <pending> }

.then()で結果を受け取る

fetch("https://jsonplaceholder.typicode.com/posts/1")
  .then((response) => {
    console.log("レスポンスを受け取りました");
    return response.json(); // これもPromiseを返す
  })
  .then((data) => {
    console.log(data.title);
  });

.then()をチェーンでつなげて、順番に処理を書けます。ただ、ネストが深くなると読みにくくなりがちです。

async/awaitで読みやすく

async/awaitを使うと、非同期コードを同期的な見た目で書けます。

async function getPost() {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
  const data = await response.json();
  console.log(data.title);
}

getPost();

awaitは「Promiseの結果が返ってくるまで待つ」という意味です。awaitを使うには、関数にasyncキーワードを付ける必要があります。

Note: この記事では主にasync/await形式で解説します。現代のJavaScript開発ではこちらが主流です。

GETリクエスト

データを取得する基本パターンを見ていきましょう。

基本形

async function fetchUser(id) {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/users/${id}`,
  );
  const user = await response.json();
  return user;
}

// 使用例
const user = await fetchUser(1);
console.log(user.name); // "Leanne Graham"

ヘッダーを付ける

APIによっては、リクエストヘッダーの指定が必要です。

async function fetchWithHeaders(url) {
  const response = await fetch(url, {
    method: "GET", // GETの場合は省略可能
    headers: {
      Accept: "application/json",
      "X-Custom-Header": "value",
    },
  });
  return response.json();
}

クエリパラメータの扱い

URLにパラメータを付けるにはURLSearchParamsが便利です。

async function searchUsers(query, limit = 10) {
  const params = new URLSearchParams({
    q: query,
    _limit: limit.toString(),
  });

  const url = `https://jsonplaceholder.typicode.com/users?${params}`;
  const response = await fetch(url);
  return response.json();
}

URLSearchParamsはエンコードも自動でやってくれます(日本語なども安全に扱えます)。

POSTリクエスト

データを送信する場合はPOSTメソッドを使います。

基本形

async function createPost(post) {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(post),
  });

  const created = await response.json();
  return created;
}

// 使用例
const newPost = await createPost({
  title: "Hello World",
  body: "This is my first post",
  userId: 1,
});
console.log(newPost.id); // 101(サーバーが割り当てたID)

重要なポイント

  1. method: "POST" を指定する
  2. Content-Type: application/json ヘッダーを付ける
  3. JSON.stringify() でオブジェクトを文字列に変換する

JSON.stringify()を忘れると、サーバーには[object Object]という文字列が送られてしまいます(よくあるミスです)。

PUT/PATCH/DELETEも同様

// PUT: リソース全体を置き換え
async function updatePost(id, post) {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/posts/${id}`,
    {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(post),
    },
  );
  return response.json();
}

// DELETE: リソースを削除
async function deletePost(id) {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/posts/${id}`,
    {
      method: "DELETE",
    },
  );
  return response.ok; // 成功ならtrue
}

エラーハンドリング

fetchのエラー処理には注意点があります。

fetchは404でも例外を投げない

const response = await fetch("https://jsonplaceholder.typicode.com/posts/9999");
console.log(response.ok); // false
console.log(response.status); // 404
// でも、例外は発生していない!

fetchが例外を投げるのは、ネットワークエラー(サーバーに到達できない場合)だけです。HTTP 404や500などのエラーレスポンスは、正常なレスポンスとして返ってきます。

response.okを確認する

async function fetchPost(id) {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/posts/${id}`,
  );

  if (!response.ok) {
    throw new Error(`HTTP Error: ${response.status}`);
  }

  return response.json();
}

response.okは、ステータスコードが200-299の範囲ならtrueになります。

try-catchで包む

async function safeFetch(url) {
  try {
    const response = await fetch(url);

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    return await response.json();
  } catch (error) {
    // ネットワークエラー or 上で投げたエラー
    console.error("Fetch failed:", error.message);
    throw error; // 再度投げるか、代替値を返す
  }
}

エラーレスポンスのボディを読む

APIによっては、エラー時にJSON形式で詳細を返すものもあります。

async function fetchWithErrorDetail(url) {
  const response = await fetch(url);

  if (!response.ok) {
    // エラーレスポンスのボディを読む
    let errorMessage = `HTTP ${response.status}`;
    try {
      const errorBody = await response.json();
      errorMessage = errorBody.message || errorMessage;
    } catch {
      // JSONでない場合は無視
    }
    throw new Error(errorMessage);
  }

  return response.json();
}

AbortControllerによるキャンセル

リクエストを途中でキャンセルしたいケースがあります(画面遷移時など)。

基本的な使い方

const controller = new AbortController();
const { signal } = controller;

// 3秒後にキャンセル
setTimeout(() => controller.abort(), 3000);

try {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts/1", {
    signal,
  });
  const data = await response.json();
  console.log(data);
} catch (error) {
  if (error.name === "AbortError") {
    console.log("リクエストがキャンセルされました");
  } else {
    throw error;
  }
}

タイムアウトの実装

async function fetchWithTimeout(url, timeoutMs = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const response = await fetch(url, { signal: controller.signal });
    clearTimeout(timeoutId);
    return response;
  } catch (error) {
    clearTimeout(timeoutId);
    if (error.name === "AbortError") {
      throw new Error(`Request timed out after ${timeoutMs}ms`);
    }
    throw error;
  }
}

Note: Reactでは、コンポーネントのアンマウント時にリクエストをキャンセルするのがベストプラクティスです。次回の「useEffectによる非同期処理」で詳しく扱います。

TypeScriptでの型付け

TypeScriptを使う場合、レスポンスの型を定義しておくと安全です。

type User = {
  id: number;
  name: string;
  email: string;
};

async function fetchUser(id: number): Promise<User> {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/users/${id}`,
  );

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  const user: User = await response.json();
  return user;
}

response.json()の戻り値はanyなので、型を明示的に付けることで、以降のコードで型チェックが効きます(タイプミスに気づけて助かります)。

やってみよう!

  1. ブラウザの開発者ツール(Console)で以下を実行してみましょう
// GETリクエスト
const res = await fetch("https://jsonplaceholder.typicode.com/users");
const users = await res.json();
console.log("ユーザー数:", users.length);
  1. POSTリクエストを試してみましょう
// POSTリクエスト
const res = await fetch("https://jsonplaceholder.typicode.com/posts", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ title: "テスト", body: "本文", userId: 1 }),
});
const created = await res.json();
console.log("作成されたID:", created.id);
  1. エラーハンドリングを確認
// 存在しないエンドポイント
const res = await fetch("https://jsonplaceholder.typicode.com/invalid");
console.log("ok:", res.ok); // false
console.log("status:", res.status); // 404
  1. タイムアウトを体験
const controller = new AbortController();
setTimeout(() => controller.abort(), 100); // 100msでキャンセル

try {
  await fetch("https://jsonplaceholder.typicode.com/posts", {
    signal: controller.signal,
  });
} catch (e) {
  console.log("エラー種別:", e.name); // "AbortError"
}

ポイント(まとめ)

  • fetch: ブラウザ標準のHTTPリクエスト関数。Promiseを返す
  • async/await: 非同期処理を同期的な見た目で書ける構文
  • response.json(): レスポンスボディをJSONとしてパース
  • response.ok: ステータスコードが200-299ならtrue
  • JSON.stringify(): POSTで送るデータをJSON文字列に変換
  • AbortController: リクエストのキャンセルに使う
  • fetchは404で例外を投げない: 必ずresponse.okをチェックする

参考リンク