はじめに

Webアプリケーションを開発する際、サーバーからデータを取得したり、フォームの内容を送信したりする場面は数多くあります。このようなサーバーとの通信を行うために、JavaScriptでは「Fetch API」が標準的に使われています。

Fetch APIは、従来のXMLHttpRequestに代わる現代的なインターフェースです。Promiseベースで設計されているため、async/await構文と組み合わせることで、直感的で読みやすいコードを書くことができます。

本記事では、以下の内容を初心者向けにわかりやすく解説します。

  • Fetch APIの基本概念と構文
  • GETリクエストでデータを取得する方法
  • POSTリクエストでデータを送信する方法
  • レスポンスの処理方法
  • エラーハンドリングのパターン
  • 実践的な活用例

Fetch APIとは

基本概念

Fetch APIは、ネットワークを通じてリソースを取得するためのJavaScriptインターフェースです。fetch()関数を呼び出すことで、HTTPリクエストを送信し、サーバーからのレスポンスを受け取ることができます。

1
2
3
4
// Fetch APIの最もシンプルな使い方
const response = await fetch("https://api.example.com/data");
const data = await response.json();
console.log(data);

fetch()Promiseを返すため、async/await.then()を使って非同期処理として扱います。

XMLHttpRequestとの違い

Fetch APIが登場する以前は、XMLHttpRequest(XHR)がサーバー通信の主な手段でした。Fetch APIには以下のような利点があります。

項目 XMLHttpRequest Fetch API
構文 コールバックベース Promiseベース
可読性 ネストが深くなりやすい シンプルで読みやすい
ストリーム対応 なし あり
async/await対応 手動でPromise化が必要 ネイティブ対応
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// XMLHttpRequestの例(従来の方法)
const xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data");
xhr.onload = function() {
  if (xhr.status === 200) {
    const data = JSON.parse(xhr.responseText);
    console.log(data);
  }
};
xhr.send();

// Fetch APIの例(現代的な方法)
const response = await fetch("https://api.example.com/data");
const data = await response.json();
console.log(data);

Fetch APIの方がコードがシンプルで、エラーハンドリングも統一的に行えます。

GETリクエストでデータを取得する

基本的なGETリクエスト

GETリクエストは、サーバーからデータを取得するための最も一般的なHTTPメソッドです。fetch()に対象のURLを渡すだけで、GETリクエストが送信されます。

1
2
3
4
5
6
7
async function fetchUsers() {
  const response = await fetch("https://jsonplaceholder.typicode.com/users");
  const users = await response.json();
  console.log(users);
}

fetchUsers();

fetch()はデフォルトでGETメソッドを使用するため、明示的に指定する必要はありません。

クエリパラメータを付与する

検索条件やページネーション情報など、追加のパラメータをURLに付与する場合は、URLSearchParamsを使うと便利です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
async function searchProducts(keyword, limit) {
  // URLSearchParamsでクエリパラメータを構築
  const params = new URLSearchParams({
    q: keyword,
    limit: limit
  });
  
  const url = `https://api.example.com/products?${params}`;
  // 結果: https://api.example.com/products?q=laptop&limit=10
  
  const response = await fetch(url);
  const products = await response.json();
  return products;
}

// 使用例
const results = await searchProducts("laptop", 10);
console.log(results);

URLSearchParamsは自動的にエンコード処理を行うため、特殊文字を含むパラメータも安全に扱えます。

レスポンスの取得と変換

fetch()が返すResponseオブジェクトには、レスポンスデータを様々な形式で取得するためのメソッドが用意されています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
async function fetchData(url) {
  const response = await fetch(url);
  
  // JSON形式で取得
  const jsonData = await response.json();
  
  // テキスト形式で取得
  // const textData = await response.text();
  
  // Blob(バイナリデータ)として取得
  // const blobData = await response.blob();
  
  // ArrayBufferとして取得
  // const bufferData = await response.arrayBuffer();
  
  return jsonData;
}

用途に応じて適切なメソッドを選択してください。Web APIとの通信では、ほとんどの場合json()メソッドを使用します。

POSTリクエストでデータを送信する

基本的なPOSTリクエスト

POSTリクエストは、サーバーにデータを送信するために使用します。fetch()の第二引数にオプションオブジェクトを渡し、methodbodyを指定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
async function createUser(userData) {
  const response = await fetch("https://api.example.com/users", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify(userData)
  });
  
  const createdUser = await response.json();
  return createdUser;
}

// 使用例
const newUser = await createUser({
  name: "田中太郎",
  email: "tanaka@example.com",
  age: 25
});
console.log("作成されたユーザー:", newUser);

JSONデータを送信する場合は、Content-Typeヘッダーをapplication/jsonに設定し、bodyにはJSON.stringify()で変換した文字列を渡します。

フォームデータの送信

HTMLフォームのデータを送信する場合は、FormDataオブジェクトを使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
async function submitForm(formElement) {
  // FormDataオブジェクトを作成
  const formData = new FormData(formElement);
  
  const response = await fetch("https://api.example.com/submit", {
    method: "POST",
    body: formData
    // FormDataを使う場合、Content-Typeは自動設定される
  });
  
  return await response.json();
}

// HTML: <form id="myForm">...</form>
const form = document.getElementById("myForm");
form.addEventListener("submit", async (e) => {
  e.preventDefault();
  const result = await submitForm(form);
  console.log("送信結果:", result);
});

FormDataを使用する場合、Content-Typeヘッダーは自動的にmultipart/form-dataとして設定されるため、明示的に指定する必要はありません。

PUT・DELETE・PATCHリクエスト

データの更新や削除には、PUT・DELETE・PATCHメソッドを使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// PUTリクエスト: リソースの完全な更新
async function updateUser(userId, userData) {
  const response = await fetch(`https://api.example.com/users/${userId}`, {
    method: "PUT",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify(userData)
  });
  return await response.json();
}

// PATCHリクエスト: リソースの部分的な更新
async function patchUser(userId, partialData) {
  const response = await fetch(`https://api.example.com/users/${userId}`, {
    method: "PATCH",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify(partialData)
  });
  return await response.json();
}

// DELETEリクエスト: リソースの削除
async function deleteUser(userId) {
  const response = await fetch(`https://api.example.com/users/${userId}`, {
    method: "DELETE"
  });
  return response.ok; // 削除成功でtrue
}

レスポンスの処理

ステータスコードの確認

fetch()は、ネットワークエラー以外ではPromiseを拒否しません。404や500などのHTTPエラーでも、Promiseは正常に解決されます。そのため、response.okresponse.statusでステータスを確認することが重要です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
async function fetchWithStatusCheck(url) {
  const response = await fetch(url);
  
  // okプロパティ: ステータスコードが200-299の範囲ならtrue
  if (!response.ok) {
    throw new Error(`HTTPエラー: ${response.status}`);
  }
  
  return await response.json();
}

主要なステータスコードの意味は以下の通りです。

ステータスコード 意味
200 OK(成功)
201 Created(リソース作成成功)
400 Bad Request(リクエスト不正)
401 Unauthorized(認証必要)
403 Forbidden(アクセス禁止)
404 Not Found(リソースなし)
500 Internal Server Error(サーバーエラー)

レスポンスヘッダーの取得

レスポンスヘッダーはresponse.headersから取得できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
async function fetchWithHeaders(url) {
  const response = await fetch(url);
  
  // 特定のヘッダーを取得
  const contentType = response.headers.get("Content-Type");
  const contentLength = response.headers.get("Content-Length");
  
  console.log("Content-Type:", contentType);
  console.log("Content-Length:", contentLength);
  
  // ヘッダーが期待する形式かチェック
  if (!contentType || !contentType.includes("application/json")) {
    throw new TypeError("レスポンスがJSON形式ではありません");
  }
  
  return await response.json();
}

エラーハンドリング

try-catchによる基本的なエラーハンドリング

fetch()のエラーハンドリングでは、ネットワークエラーとHTTPエラーを区別して処理することが重要です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
async function fetchData(url) {
  try {
    const response = await fetch(url);
    
    // HTTPエラーのチェック
    if (!response.ok) {
      throw new Error(`HTTPエラー: ${response.status} ${response.statusText}`);
    }
    
    const data = await response.json();
    return { success: true, data };
    
  } catch (error) {
    // ネットワークエラーまたはHTTPエラー
    console.error("データ取得エラー:", error.message);
    return { success: false, error: error.message };
  }
}

// 使用例
const result = await fetchData("https://api.example.com/data");
if (result.success) {
  console.log("取得データ:", result.data);
} else {
  console.log("エラー発生:", result.error);
}

エラーの種類を判別する

より細かいエラー処理が必要な場合は、エラーの種類を判別します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
async function fetchWithDetailedErrorHandling(url) {
  try {
    const response = await fetch(url);
    
    if (!response.ok) {
      // HTTPステータスに応じたエラー処理
      switch (response.status) {
        case 400:
          throw new Error("リクエストが不正です。入力内容を確認してください。");
        case 401:
          throw new Error("認証が必要です。ログインしてください。");
        case 403:
          throw new Error("このリソースへのアクセス権限がありません。");
        case 404:
          throw new Error("指定されたリソースが見つかりません。");
        case 500:
          throw new Error("サーバーでエラーが発生しました。しばらくお待ちください。");
        default:
          throw new Error(`予期しないエラー: ${response.status}`);
      }
    }
    
    return await response.json();
    
  } catch (error) {
    if (error instanceof TypeError) {
      // ネットワークエラー(オフライン、DNS解決失敗など)
      console.error("ネットワークエラー:", error.message);
    } else {
      // その他のエラー(HTTPエラー、JSONパースエラーなど)
      console.error("エラー:", error.message);
    }
    throw error;
  }
}

カスタムエラークラスの活用

大規模なアプリケーションでは、カスタムエラークラスを定義すると管理しやすくなります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// カスタムエラークラスの定義
class ApiError extends Error {
  constructor(message, status, data = null) {
    super(message);
    this.name = "ApiError";
    this.status = status;
    this.data = data;
  }
}

// APIクライアント関数
async function apiRequest(url, options = {}) {
  try {
    const response = await fetch(url, options);
    
    // レスポンスボディを取得(エラー時も含む)
    let data;
    const contentType = response.headers.get("Content-Type");
    if (contentType && contentType.includes("application/json")) {
      data = await response.json();
    } else {
      data = await response.text();
    }
    
    if (!response.ok) {
      throw new ApiError(
        data.message || `HTTP ${response.status}`,
        response.status,
        data
      );
    }
    
    return data;
    
  } catch (error) {
    if (error instanceof ApiError) {
      throw error;
    }
    // ネットワークエラーなどをApiErrorでラップ
    throw new ApiError(error.message, 0);
  }
}

// 使用例
try {
  const users = await apiRequest("https://api.example.com/users");
  console.log(users);
} catch (error) {
  if (error instanceof ApiError) {
    console.log("ステータス:", error.status);
    console.log("メッセージ:", error.message);
    console.log("詳細データ:", error.data);
  }
}

リクエストのカスタマイズ

ヘッダーの設定

認証トークンやカスタムヘッダーを設定する方法です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
async function fetchWithAuth(url, token) {
  const response = await fetch(url, {
    method: "GET",
    headers: {
      "Authorization": `Bearer ${token}`,
      "Content-Type": "application/json",
      "X-Custom-Header": "custom-value"
    }
  });
  
  return await response.json();
}

タイムアウトの設定

AbortControllerを使用すると、リクエストにタイムアウトを設定できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
async function fetchWithTimeout(url, timeout = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);
  
  try {
    const response = await fetch(url, {
      signal: controller.signal
    });
    clearTimeout(timeoutId);
    return await response.json();
    
  } catch (error) {
    clearTimeout(timeoutId);
    if (error.name === "AbortError") {
      throw new Error("リクエストがタイムアウトしました");
    }
    throw error;
  }
}

// 3秒でタイムアウトする設定
try {
  const data = await fetchWithTimeout("https://api.example.com/data", 3000);
  console.log(data);
} catch (error) {
  console.error(error.message);
}

リクエストの中止

ユーザー操作などでリクエストを中止する場合もAbortControllerを使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 検索機能での活用例
let currentController = null;

async function search(keyword) {
  // 前回のリクエストがあればキャンセル
  if (currentController) {
    currentController.abort();
  }
  
  currentController = new AbortController();
  
  try {
    const response = await fetch(
      `https://api.example.com/search?q=${encodeURIComponent(keyword)}`,
      { signal: currentController.signal }
    );
    
    if (!response.ok) {
      throw new Error(`検索エラー: ${response.status}`);
    }
    
    return await response.json();
    
  } catch (error) {
    if (error.name === "AbortError") {
      console.log("検索がキャンセルされました");
      return null;
    }
    throw error;
  }
}

実践的な活用例

ユーザー一覧を取得して表示する

実際のWebアプリケーションでよくあるパターンです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// APIからユーザー一覧を取得してDOMに表示する例
async function displayUsers() {
  const container = document.getElementById("user-list");
  container.innerHTML = "<p>読み込み中...</p>";
  
  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/users");
    
    if (!response.ok) {
      throw new Error("ユーザー情報の取得に失敗しました");
    }
    
    const users = await response.json();
    
    // ユーザー一覧をHTMLとして構築
    const html = users.map(user => `
      <div class="user-card">
        <h3>${user.name}</h3>
        <p>メール: ${user.email}</p>
        <p>会社: ${user.company.name}</p>
      </div>
    `).join("");
    
    container.innerHTML = html;
    
  } catch (error) {
    container.innerHTML = `<p class="error">エラー: ${error.message}</p>`;
  }
}

// ページ読み込み時に実行
displayUsers();

フォーム送信とバリデーション

フォームデータの送信と結果の処理を行う例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// お問い合わせフォームの送信処理
const contactForm = document.getElementById("contact-form");
const submitButton = document.getElementById("submit-button");
const messageArea = document.getElementById("message");

contactForm.addEventListener("submit", async (e) => {
  e.preventDefault();
  
  // 送信中の状態を表示
  submitButton.disabled = true;
  submitButton.textContent = "送信中...";
  messageArea.textContent = "";
  
  try {
    const formData = {
      name: document.getElementById("name").value,
      email: document.getElementById("email").value,
      message: document.getElementById("message-input").value
    };
    
    const response = await fetch("https://api.example.com/contact", {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify(formData)
    });
    
    const result = await response.json();
    
    if (!response.ok) {
      throw new Error(result.message || "送信に失敗しました");
    }
    
    // 送信成功
    messageArea.textContent = "お問い合わせを受け付けました。";
    messageArea.className = "success";
    contactForm.reset();
    
  } catch (error) {
    // エラー表示
    messageArea.textContent = error.message;
    messageArea.className = "error";
    
  } finally {
    // ボタンを元に戻す
    submitButton.disabled = false;
    submitButton.textContent = "送信";
  }
});

再利用可能なAPIクライアントの作成

アプリケーション全体で使いまわせるAPIクライアントを作成する例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// apiClient.js
const BASE_URL = "https://api.example.com";

const apiClient = {
  // 共通のfetch処理
  async request(endpoint, options = {}) {
    const url = `${BASE_URL}${endpoint}`;
    
    const config = {
      headers: {
        "Content-Type": "application/json",
        ...options.headers
      },
      ...options
    };
    
    // 認証トークンがあれば追加
    const token = localStorage.getItem("authToken");
    if (token) {
      config.headers["Authorization"] = `Bearer ${token}`;
    }
    
    const response = await fetch(url, config);
    
    if (!response.ok) {
      const errorData = await response.json().catch(() => ({}));
      throw new Error(errorData.message || `HTTP ${response.status}`);
    }
    
    return response.json();
  },
  
  // GETリクエスト
  get(endpoint) {
    return this.request(endpoint);
  },
  
  // POSTリクエスト
  post(endpoint, data) {
    return this.request(endpoint, {
      method: "POST",
      body: JSON.stringify(data)
    });
  },
  
  // PUTリクエスト
  put(endpoint, data) {
    return this.request(endpoint, {
      method: "PUT",
      body: JSON.stringify(data)
    });
  },
  
  // DELETEリクエスト
  delete(endpoint) {
    return this.request(endpoint, {
      method: "DELETE"
    });
  }
};

// 使用例
async function main() {
  try {
    // ユーザー一覧を取得
    const users = await apiClient.get("/users");
    console.log(users);
    
    // 新規ユーザーを作成
    const newUser = await apiClient.post("/users", {
      name: "新規ユーザー",
      email: "new@example.com"
    });
    console.log(newUser);
    
    // ユーザー情報を更新
    const updatedUser = await apiClient.put("/users/1", {
      name: "更新されたユーザー"
    });
    console.log(updatedUser);
    
    // ユーザーを削除
    await apiClient.delete("/users/1");
    console.log("削除完了");
    
  } catch (error) {
    console.error("APIエラー:", error.message);
  }
}

Fetch APIの処理フロー

Fetch APIを使った通信の全体的な流れを図で表すと以下のようになります。

flowchart TD
    A[fetch関数を呼び出す] --> B[リクエスト送信]
    B --> C{ネットワーク<br/>エラー?}
    C -->|はい| D[Promiseが拒否<br/>TypeErrorが発生]
    C -->|いいえ| E[Responseオブジェクトを受信]
    E --> F{response.ok<br/>をチェック}
    F -->|true<br/>200-299| G[レスポンスボディを取得<br/>json / text / blob]
    F -->|false<br/>400-599| H[エラー処理<br/>ステータスに応じた対応]
    G --> I[データを処理して完了]
    H --> J[エラーをthrowまたは<br/>エラーメッセージ表示]
    D --> K[catchブロックで処理]

まとめ

本記事では、JavaScriptのFetch APIを使ったWeb API通信の基本を解説しました。

学習したポイントをまとめます。

  • Fetch APIの基本: fetch()関数はPromiseを返し、async/awaitと組み合わせて直感的に使える
  • GETリクエスト: URLを渡すだけでデータ取得が可能、クエリパラメータはURLSearchParamsで構築
  • POSTリクエスト: methodとbodyオプションを指定し、Content-Typeヘッダーを適切に設定
  • レスポンス処理: response.okでステータスを確認し、json()などのメソッドでデータを取得
  • エラーハンドリング: ネットワークエラーとHTTPエラーを区別して処理することが重要
  • リクエストのカスタマイズ: AbortControllerでタイムアウトやキャンセルを実装できる

Fetch APIはモダンなWebアプリケーション開発に欠かせない技術です。本記事で紹介したパターンを参考に、実際のプロジェクトでWeb API通信を実装してみてください。

参考リンク