Fetch API

モダンブラウザとNode.js 18+で利用可能なネイティブHTTPクライアントAPI。軽量でゼロ依存関係、標準化されたWebプラットフォームAPI。プロミスベースで、Request/Responseオブジェクトによる柔軟な制御とストリーミング対応を提供する。

HTTPクライアントJavaScriptネイティブAPIWeb標準Promiseストリーム

GitHub概要

web-platform-tests/wpt

Test suites for Web platform specs — including WHATWG, W3C, and others

スター5,581
ウォッチ389
フォーク3,503
作成日:2012年3月4日
言語:HTML
ライセンス:Other

トピックス

blinkbrowserdomfirefoxgeckogoogle-chromehtmljavascriptmicrosoft-edgeoperasafaritest-automationtest-runnertestingw3cweb-developmentweb-standardswebkitwhatwg

スター履歴

web-platform-tests/wpt Star History
データ取得日時: 2025/10/22 10:04

ライブラリ

Fetch API

概要

Fetch APIは「モダンブラウザとNode.js 18+で利用可能なネイティブHTTPクライアントAPI」として標準化されたWebプラットフォームAPIです。軽量でゼロ依存関係、プロミスベースの設計により、XMLHttpRequestの現代的な置き換えとして位置づけられています。Request/Responseオブジェクトによる柔軟な制御とストリーミング対応を提供し、Edge Computing環境やバンドルサイズを重視するプロジェクトで特に威力を発揮します。

詳細

Fetch API 2025年版はモダンJavaScript開発における重要な転換点を象徴しています。Node.js 18+での正式サポートにより、ブラウザとサーバー環境で統一されたAPIが利用可能となり、依存関係最小化トレンドを牽引しています。UndiciをベースとしたNode.js実装により高いパフォーマンスを実現し、Next.js、Vite等のモダンフレームワークでデフォルト選択となっています。Promise-basedインターフェース、ストリーミングサポート、CORS対応など現代Webアプリケーションに必要な機能を網羅しています。

主な特徴

  • ネイティブ実装: ブラウザとNode.js環境でのネイティブサポート
  • ゼロ依存関係: 外部ライブラリ不要の軽量実装
  • Promise-based: async/awaitとの完全な統合
  • ストリーミング対応: 大容量データの効率的な処理
  • 標準準拠: Web標準に基づく一貫したAPI
  • 高性能: Undiciエンジンによる最適化された通信処理

メリット・デメリット

メリット

  • Web標準に基づく将来性の高い設計で長期的な安定性を保証
  • バンドルサイズゼロによる軽量アプリケーション実現
  • ブラウザとサーバー間でのコード共有とポータビリティ向上
  • モダンフレームワークでのデフォルト採用による生態系統合
  • Promise-basedによる直感的な非同期処理
  • ストリーミング対応による大容量データの効率的処理

デメリット

  • 古いブラウザ(IE11等)では利用不可、ポリフィルが必要
  • Node.js 18未満の環境では利用不可
  • リクエスト/レスポンスインターセプター機能なし
  • 自動JSON変換やタイムアウト機能の標準サポートなし
  • エラーハンドリングが4xxや5xxステータスで自動的にrejectしない
  • 高度な設定オプションはAxios等に比べて限定的

参考ページ

書き方の例

インストールと基本セットアップ

# Fetch APIはネイティブ実装のためインストール不要
# ブラウザ: 全モダンブラウザで利用可能
# Node.js: 18.0.0以降でネイティブサポート

# 古いNode.jsバージョンでのポリフィル(非推奨)
# npm install node-fetch

# TypeScript型定義(通常は不要、標準で含まれる)
# npm install --save-dev @types/node

基本的なリクエスト(GET/POST/PUT/DELETE)

// 基本的なGETリクエスト
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);

// JSONデータの送信(POST)
const postData = {
  name: '田中太郎',
  email: '[email protected]'
};

const postResponse = await fetch('https://api.example.com/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(postData)
});

const result = await postResponse.json();
console.log('作成されたユーザー:', result);

// PUTリクエスト(更新)
const updateResponse = await fetch('https://api.example.com/users/123', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer your-token'
  },
  body: JSON.stringify({
    name: '田中次郎',
    email: '[email protected]'
  })
});

// DELETEリクエスト
const deleteResponse = await fetch('https://api.example.com/users/123', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer your-token'
  }
});

if (deleteResponse.ok) {
  console.log('削除完了');
}

// 複数形式でのレスポンス取得
const apiResponse = await fetch('https://api.example.com/data');

// JSONとして取得
const jsonData = await apiResponse.clone().json();

// テキストとして取得
const textData = await apiResponse.clone().text();

// ArrayBufferとして取得
const bufferData = await apiResponse.clone().arrayBuffer();

// Blobとして取得
const blobData = await apiResponse.clone().blob();

高度な設定とカスタマイズ(ヘッダー、認証、タイムアウト等)

// カスタムヘッダーと認証
const authenticatedRequest = await fetch('https://api.example.com/private', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer your-jwt-token',
    'Accept': 'application/json',
    'User-Agent': 'MyApp/1.0',
    'X-Custom-Header': 'custom-value'
  },
  credentials: 'include', // Cookieを含める
  mode: 'cors', // CORS設定
  cache: 'no-cache' // キャッシュ設定
});

// タイムアウト機能の実装
function fetchWithTimeout(url, options = {}, timeout = 5000) {
  return Promise.race([
    fetch(url, options),
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error('リクエストタイムアウト')), timeout)
    )
  ]);
}

// 使用例
try {
  const response = await fetchWithTimeout('https://api.example.com/slow-endpoint', {
    method: 'GET',
    headers: {
      'Authorization': 'Bearer token'
    }
  }, 3000); // 3秒でタイムアウト

  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error('エラー:', error.message);
}

// AbortControllerを使用したリクエストキャンセル
const controller = new AbortController();
const signal = controller.signal;

// 5秒後にリクエストをキャンセル
setTimeout(() => controller.abort(), 5000);

try {
  const response = await fetch('https://api.example.com/data', {
    method: 'GET',
    signal: signal,
    headers: {
      'Accept': 'application/json'
    }
  });

  const data = await response.json();
  console.log(data);
} catch (error) {
  if (error.name === 'AbortError') {
    console.log('リクエストがキャンセルされました');
  } else {
    console.error('エラー:', error);
  }
}

// プロキシ設定(Node.js環境)
const httpsProxyAgent = require('https-proxy-agent');

const proxyResponse = await fetch('https://api.example.com/data', {
  method: 'GET',
  agent: new httpsProxyAgent('http://proxy.example.com:8080')
});

エラーハンドリングとリトライ機能

// 詳細なエラーハンドリング
async function fetchWithErrorHandling(url, options = {}) {
  try {
    const response = await fetch(url, options);

    // Fetch APIは4xx/5xxでもrejectしないため手動チェック
    if (!response.ok) {
      // ステータスコード別の詳細なエラーハンドリング
      switch (response.status) {
        case 400:
          throw new Error('リクエストが無効です (400 Bad Request)');
        case 401:
          throw new Error('認証が必要です (401 Unauthorized)');
        case 403:
          throw new Error('アクセスが拒否されました (403 Forbidden)');
        case 404:
          throw new Error('リソースが見つかりません (404 Not Found)');
        case 429:
          throw new Error('レート制限に達しました (429 Too Many Requests)');
        case 500:
          throw new Error('サーバーエラーが発生しました (500 Internal Server Error)');
        default:
          throw new Error(`HTTPエラー: ${response.status} ${response.statusText}`);
      }
    }

    return response;
  } catch (error) {
    // ネットワークエラーやその他の例外
    if (error instanceof TypeError) {
      throw new Error('ネットワークエラー: サーバーに接続できません');
    }
    throw error;
  }
}

// 指数バックオフによるリトライ機能
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetchWithErrorHandling(url, options);
      return response;
    } catch (error) {
      console.log(`試行 ${attempt}/${maxRetries} 失敗:`, error.message);

      if (attempt === maxRetries) {
        throw new Error(`${maxRetries}回の試行後も失敗: ${error.message}`);
      }

      // 指数バックオフ(1秒, 2秒, 4秒...)
      const delay = Math.pow(2, attempt - 1) * 1000;
      console.log(`${delay}ms待機後に再試行...`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

// 使用例
try {
  const response = await fetchWithRetry('https://api.example.com/unstable-endpoint', {
    method: 'GET',
    headers: {
      'Authorization': 'Bearer token'
    }
  });

  const data = await response.json();
  console.log('取得成功:', data);
} catch (error) {
  console.error('最終的に失敗:', error.message);
}

// ステータス確認とカスタムバリデーション
async function validateResponse(response) {
  if (!response.ok) {
    const errorBody = await response.text();
    throw new Error(`HTTP ${response.status}: ${errorBody}`);
  }

  const contentType = response.headers.get('content-type');
  if (!contentType || !contentType.includes('application/json')) {
    throw new Error('レスポンスがJSON形式ではありません');
  }

  return response;
}

並行処理と非同期リクエスト

// 複数リクエストの並列実行
async function fetchMultipleEndpoints() {
  const urls = [
    'https://api.example.com/users',
    'https://api.example.com/posts',
    'https://api.example.com/comments'
  ];

  try {
    // Promise.allを使用した並列実行
    const responses = await Promise.all(
      urls.map(url => fetch(url, {
        headers: {
          'Authorization': 'Bearer token'
        }
      }))
    );

    // 全レスポンスのJSONを並列取得
    const data = await Promise.all(
      responses.map(response => response.json())
    );

    const [users, posts, comments] = data;
    console.log('ユーザー:', users);
    console.log('投稿:', posts);
    console.log('コメント:', comments);

    return { users, posts, comments };
  } catch (error) {
    console.error('並列リクエストエラー:', error);
    throw error;
  }
}

// Promise.allSettledを使用した部分失敗許容パターン
async function fetchWithPartialFailure() {
  const requests = [
    fetch('https://api.example.com/reliable-endpoint'),
    fetch('https://api.example.com/unreliable-endpoint'),
    fetch('https://api.example.com/another-endpoint')
  ];

  const results = await Promise.allSettled(requests);

  const successfulResponses = [];
  const errors = [];

  for (let i = 0; i < results.length; i++) {
    const result = results[i];
    if (result.status === 'fulfilled') {
      try {
        const data = await result.value.json();
        successfulResponses.push(data);
      } catch (parseError) {
        errors.push(`Response ${i}: JSON parse error`);
      }
    } else {
      errors.push(`Request ${i}: ${result.reason.message}`);
    }
  }

  console.log('成功したレスポンス:', successfulResponses);
  console.log('エラー:', errors);

  return { successfulResponses, errors };
}

// 段階的データ取得(ページネーション対応)
async function fetchAllPages(baseUrl, pageSize = 20) {
  const allData = [];
  let page = 1;
  let hasMore = true;

  while (hasMore) {
    const url = `${baseUrl}?page=${page}&limit=${pageSize}`;
    
    try {
      const response = await fetch(url, {
        headers: {
          'Authorization': 'Bearer token'
        }
      });

      if (!response.ok) {
        throw new Error(`Page ${page} request failed: ${response.status}`);
      }

      const pageData = await response.json();
      allData.push(...pageData.items);

      // ページネーション情報の確認
      hasMore = pageData.hasMore || pageData.items.length === pageSize;
      page++;

      console.log(`Page ${page - 1} 取得完了: ${pageData.items.length}件`);

      // API負荷軽減のための待機
      await new Promise(resolve => setTimeout(resolve, 100));
    } catch (error) {
      console.error(`Page ${page} でエラー:`, error);
      break;
    }
  }

  console.log(`全${allData.length}件のデータを取得完了`);
  return allData;
}

フレームワーク統合と実用例

// React環境での使用例
import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

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

    async function fetchUser() {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`, {
          signal: controller.signal,
          headers: {
            'Authorization': `Bearer ${localStorage.getItem('token')}`
          }
        });

        if (!response.ok) {
          throw new Error('ユーザー情報の取得に失敗しました');
        }

        const userData = await response.json();
        setUser(userData);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        setLoading(false);
      }
    }

    fetchUser();

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

  if (loading) return <div>読み込み中...</div>;
  if (error) return <div>エラー: {error}</div>;
  if (!user) return <div>ユーザーが見つかりません</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

// Next.js API Routes での使用
export default async function handler(req, res) {
  if (req.method === 'GET') {
    try {
      const response = await fetch('https://external-api.example.com/data', {
        headers: {
          'Authorization': `Bearer ${process.env.API_TOKEN}`
        }
      });

      if (!response.ok) {
        return res.status(response.status).json({ 
          error: 'External API error' 
        });
      }

      const data = await response.json();
      res.status(200).json(data);
    } catch (error) {
      res.status(500).json({ error: 'Server error' });
    }
  } else {
    res.setHeader('Allow', ['GET']);
    res.status(405).end(`Method ${req.method} Not Allowed`);
  }
}

// Express.js サーバーでの使用
const express = require('express');
const app = express();

app.get('/api/proxy/:endpoint', async (req, res) => {
  try {
    const { endpoint } = req.params;
    const response = await fetch(`https://api.example.com/${endpoint}`, {
      headers: {
        'Authorization': req.headers.authorization,
        'Content-Type': 'application/json'
      }
    });

    const data = await response.json();
    res.json(data);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// ファイルアップロード(FormData)
async function uploadFile(file, additionalData = {}) {
  const formData = new FormData();
  formData.append('file', file);
  
  // 追加データの付与
  Object.entries(additionalData).forEach(([key, value]) => {
    formData.append(key, value);
  });

  try {
    const response = await fetch('/api/upload', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer token'
        // Content-Typeは自動設定されるため指定しない
      },
      body: formData
    });

    if (!response.ok) {
      throw new Error('アップロードに失敗しました');
    }

    return await response.json();
  } catch (error) {
    console.error('アップロードエラー:', error);
    throw error;
  }
}

// ストリーミングレスポンスの処理
async function handleStreamingResponse() {
  const response = await fetch('https://api.example.com/stream');
  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  try {
    while (true) {
      const { done, value } = await reader.read();
      
      if (done) break;
      
      const chunk = decoder.decode(value, { stream: true });
      console.log('受信チャンク:', chunk);
      
      // リアルタイムでUIを更新
      updateUI(chunk);
    }
  } finally {
    reader.releaseLock();
  }
}