Ky

TypeScript完全対応の軽量HTTPクライアント。Fetch APIベースでありながら、より使いやすいAPIと拡張機能を提供。厳密な型定義、自動リトライ、タイムアウト、JSON処理、hooks機能を内蔵。モダンJavaScript/TypeScript環境に最適化。

HTTPクライアントJavaScriptTypeScriptFetch API軽量Promise

GitHub概要

sindresorhus/ky

🌳 Tiny & elegant JavaScript HTTP client based on the Fetch API

スター15,080
ウォッチ60
フォーク410
作成日:2018年9月4日
言語:TypeScript
ライセンス:MIT License

トピックス

fetchhttp-clienthttp-requestjavascriptjsjsonnpm-packagerequestresttinywhatwg-fetch

スター履歴

sindresorhus/ky Star History
データ取得日時: 2025/7/18 06:02

ライブラリ

Ky

概要

Kyは「モダンブラウザ、Node.js、Deno向けの軽量HTTPクライアント」として開発されたライブラリです。Fetch APIをベースに構築されながら、より使いやすいAPIと拡張機能を提供します。TypeScript完全サポート、自動リトライ、タイムアウト、JSON処理、hooks機能を内蔵し、Axiosの軽量代替として注目されています。Promise-basedでチェーン可能なAPIにより、モダンJavaScript開発で直感的なHTTPリクエスト処理を実現します。

詳細

Ky 2025年版はFetch APIの改良版として急速に注目度が上昇しているHTTPクライアントライブラリです。TypeScript開発者とモダンJavaScript環境で高く評価され、バンドルサイズと開発者体験の両立を実現しています。標準のFetch APIに加えて、自動リトライ、エラーハンドリング、進捗追跡、リクエスト/レスポンスフック機能を提供。ブラウザ、Node.js、Denoでの統一されたAPIにより、ユニバーサルJavaScriptアプリケーション開発を支援します。

主な特徴

  • Fetch API基盤: モダンブラウザの標準APIをベースとした軽量設計
  • TypeScript完全対応: 型安全なAPI設計と開発時補完機能
  • 自動リトライ: 設定可能な指数バックオフによるリトライ機能
  • Hooks システム: リクエスト/レスポンスライフサイクルへの介入機能
  • 進捗追跡: アップロード/ダウンロード進捗のリアルタイム監視
  • 統一API: ブラウザ、Node.js、Denoでの一貫したインターフェース

メリット・デメリット

メリット

  • Fetch APIベースによる軽量性とモダンブラウザでの高いパフォーマンス
  • TypeScript完全サポートによる型安全性と開発効率向上
  • 自動リトライとエラーハンドリングによる堅牢なネットワーク処理
  • Hooks システムによる柔軟なリクエスト/レスポンス処理カスタマイズ
  • 直感的なチェーン可能APIによる読みやすいコード記述
  • Axiosより小さなバンドルサイズでパフォーマンス向上

デメリット

  • 古いブラウザ(Fetch API非対応)では利用不可
  • Node.js 18未満では追加のポリフィルが必要
  • Axiosと比較して機能セットは限定的
  • エコシステムとプラグインはAxiosほど豊富ではない
  • 学習リソースと例がAxiosより少ない
  • 一部の高度なリクエスト設定機能が制限的

参考ページ

書き方の例

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

# Kyのインストール
npm install ky

# Denoでの使用(URLインポート)
# import ky from 'https://esm.sh/ky';

# TypeScript型定義は自動的に含まれる
# @types/kyは不要

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

// ES6 modulesでのインポート
import ky from 'ky';

// 基本的なGETリクエスト
const data = await ky.get('https://api.example.com/users').json();
console.log('ユーザーデータ:', data);

// TypeScript型指定付きGETリクエスト
interface User {
  id: number;
  name: string;
  email: string;
}

const users = await ky.get<User[]>('https://api.example.com/users').json();
console.log('型安全なユーザーデータ:', users);

// POSTリクエスト(JSON送信)
const newUser = await ky.post('https://api.example.com/users', {
  json: {
    name: '田中太郎',
    email: '[email protected]'
  }
}).json();

console.log('作成されたユーザー:', newUser);

// PUTリクエスト(更新)
const updatedUser = await ky.put('https://api.example.com/users/123', {
  json: {
    name: '田中次郎',
    email: '[email protected]'
  }
}).json();

// DELETEリクエスト
await ky.delete('https://api.example.com/users/123');
console.log('ユーザーを削除しました');

// 様々なレスポンス形式の取得
const textResponse = await ky.get('https://api.example.com/message').text();
const blobResponse = await ky.get('https://api.example.com/file').blob();
const bufferResponse = await ky.get('https://api.example.com/binary').arrayBuffer();

// チェーン可能なAPIの活用
const response = await ky
  .get('https://api.example.com/data')
  .json();

// ステータスコードとヘッダーの確認
const httpResponse = await ky.get('https://api.example.com/status');
console.log('ステータス:', httpResponse.status);
console.log('ヘッダー:', Object.fromEntries(httpResponse.headers));

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

// カスタムヘッダーと認証設定
const authenticatedData = await ky.get('https://api.example.com/private', {
  headers: {
    'Authorization': 'Bearer your-jwt-token',
    'Accept': 'application/json',
    'User-Agent': 'MyApp/1.0',
    'X-Custom-Header': 'custom-value'
  }
}).json();

// タイムアウト設定
try {
  const data = await ky.get('https://api.example.com/slow-endpoint', {
    timeout: 5000 // 5秒でタイムアウト
  }).json();
} catch (error) {
  if (error.name === 'TimeoutError') {
    console.log('リクエストがタイムアウトしました');
  }
}

// Search parameters(クエリパラメータ)の設定
const searchData = await ky.get('https://api.example.com/search', {
  searchParams: {
    q: 'JavaScript',
    page: 1,
    limit: 10,
    sort: 'created_at'
  }
}).json();

// URLSearchParamsを使用したクエリパラメータ
const params = new URLSearchParams();
params.set('category', 'tech');
params.set('published', 'true');

const posts = await ky.get('https://api.example.com/posts', {
  searchParams: params
}).json();

// prefixUrl を使用したベースURL設定
const api = ky.create({
  prefixUrl: 'https://api.example.com/v1'
});

const user = await api.get('users/123').json();
// 実際のURL: https://api.example.com/v1/users/123

// AbortControllerによるリクエストキャンセル
const controller = new AbortController();

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

try {
  const data = await ky.get('https://api.example.com/data', {
    signal: controller.signal
  }).json();
} catch (error) {
  if (error.name === 'AbortError') {
    console.log('リクエストがキャンセルされました');
  }
}

// カスタムContent-Type設定
const response = await ky.post('https://api.example.com/data', {
  headers: {
    'Content-Type': 'application/vnd.api+json'
  },
  json: {
    data: {
      type: 'articles',
      attributes: {
        title: 'カスタムコンテンツタイプの例'
      }
    }
  }
});

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

// 詳細なエラーハンドリング
try {
  const data = await ky.get('https://api.example.com/users').json();
} catch (error) {
  if (error.name === 'HTTPError') {
    console.log('HTTPエラー:', error.response.status);
    console.log('レスポンス:', await error.response.text());
    
    // ステータスコード別の処理
    switch (error.response.status) {
      case 400:
        console.error('リクエストが無効です');
        break;
      case 401:
        console.error('認証が必要です');
        break;
      case 404:
        console.error('リソースが見つかりません');
        break;
      case 500:
        console.error('サーバーエラーです');
        break;
    }
  } else if (error.name === 'TimeoutError') {
    console.error('リクエストがタイムアウトしました');
  } else {
    console.error('ネットワークエラー:', error.message);
  }
}

// リトライ設定のカスタマイズ
const data = await ky.get('https://api.example.com/unstable-endpoint', {
  retry: {
    limit: 5, // 最大5回リトライ
    methods: ['get', 'post'], // GETとPOSTメソッドでリトライ
    statusCodes: [408, 413, 429, 500, 502, 503, 504], // リトライ対象ステータス
    backoffLimit: 3000, // 最大遅延時間(3秒)
    delay: attemptCount => Math.min(1000 * (2 ** attemptCount), 3000) // カスタム遅延関数
  }
}).json();

// シンプルなリトライ制限設定
const simpleRetryData = await ky.get('https://api.example.com/data', {
  retry: 3 // 3回までリトライ
}).json();

// beforeErrorフックでエラーカスタマイズ
const customErrorData = await ky.get('https://api.example.com/data', {
  hooks: {
    beforeError: [
      error => {
        const { response } = error;
        if (response && response.body) {
          error.name = 'CustomAPIError';
          error.message = `APIエラー: ${response.status} - ${response.statusText}`;
        }
        return error;
      }
    ]
  }
}).json();

// エラーを投げずにレスポンスを取得
const responseWithoutThrow = await ky.get('https://api.example.com/maybe-error', {
  throwHttpErrors: false
});

if (!responseWithoutThrow.ok) {
  console.log('エラーレスポンス:', responseWithoutThrow.status);
  const errorData = await responseWithoutThrow.json();
  console.log('エラー詳細:', errorData);
} else {
  const successData = await responseWithoutThrow.json();
  console.log('成功データ:', successData);
}

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

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

    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 = [
    ky.get('https://api.example.com/reliable').json(),
    ky.get('https://api.example.com/unreliable').json(),
    ky.get('https://api.example.com/another').json()
  ];

  const results = await Promise.allSettled(requests);

  const successful = results
    .filter(result => result.status === 'fulfilled')
    .map(result => result.value);

  const failed = results
    .filter(result => result.status === 'rejected')
    .map(result => result.reason);

  console.log('成功したリクエスト:', successful);
  console.log('失敗したリクエスト:', failed);

  return { successful, failed };
}

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

  while (hasMore) {
    try {
      const pageData = await ky.get(baseUrl, {
        searchParams: {
          page: page,
          limit: 20
        }
      }).json();

      allData.push(...pageData.items);
      hasMore = pageData.hasMore;
      page++;

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

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

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

// 条件付きリクエスト実行
async function conditionalRequests() {
  const results = await Promise.allSettled([
    ky.get('https://api.example.com/endpoint1').json(),
    ky.get('https://api.example.com/endpoint2').json()
  ]);

  // 最初のリクエストが成功した場合のみ追加処理
  if (results[0].status === 'fulfilled') {
    const additionalData = await ky.get('https://api.example.com/additional', {
      searchParams: {
        id: results[0].value.id
      }
    }).json();

    return {
      main: results[0].value,
      additional: additionalData,
      secondary: results[1].status === 'fulfilled' ? results[1].value : null
    };
  }

  return { error: 'メインリクエストが失敗しました' };
}

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

// React Hooksでの統合例
import { useState, useEffect } from 'react';
import ky from 'ky';

function useApi(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

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

    async function fetchData() {
      try {
        setLoading(true);
        setError(null);

        const result = await ky.get(url, {
          ...options,
          signal: controller.signal
        }).json();

        setData(result);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err);
        }
      } finally {
        setLoading(false);
      }
    }

    fetchData();

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

  return { data, loading, error };
}

// React コンポーネントでの使用
function UserProfile({ userId }) {
  const { data: user, loading, error } = useApi(
    `https://api.example.com/users/${userId}`,
    {
      headers: {
        'Authorization': `Bearer ${localStorage.getItem('token')}`
      }
    }
  );

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

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

// APIクライアントインスタンスの作成と拡張
const apiClient = ky.create({
  prefixUrl: 'https://api.example.com/v1',
  headers: {
    'Accept': 'application/json'
  },
  timeout: 10000,
  retry: 2
});

// 認証付きAPIクライアント
const authenticatedApi = apiClient.extend({
  hooks: {
    beforeRequest: [
      request => {
        const token = localStorage.getItem('authToken');
        if (token) {
          request.headers.set('Authorization', `Bearer ${token}`);
        }
      }
    ]
  }
});

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

  try {
    const response = await ky.post('https://api.example.com/upload', {
      body: formData,
      onUploadProgress: (progress, chunk) => {
        const percent = Math.round(progress.percent * 100);
        console.log(`アップロード進捗: ${percent}%`);
        console.log(`転送済み: ${progress.transferredBytes}/${progress.totalBytes} bytes`);
      }
    });

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

// ダウンロード進捗追跡
async function downloadFile(url, filename) {
  try {
    const response = await ky.get(url, {
      onDownloadProgress: (progress, chunk) => {
        const percent = Math.round(progress.percent * 100);
        console.log(`ダウンロード進捗: ${percent}%`);
        updateProgressBar(percent);
      }
    });

    const blob = await response.blob();
    
    // ブラウザでファイルダウンロード
    const downloadUrl = window.URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = downloadUrl;
    link.download = filename;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    window.URL.revokeObjectURL(downloadUrl);

    console.log('ダウンロード完了');
  } catch (error) {
    console.error('ダウンロードエラー:', error);
    throw error;
  }
}

// Hooksを使用した高度な処理
const advancedApi = ky.create({
  prefixUrl: 'https://api.example.com',
  hooks: {
    beforeRequest: [
      request => {
        // リクエスト前の共通処理
        request.headers.set('X-Request-ID', generateRequestId());
        console.log('リクエスト送信:', request.url);
      }
    ],
    afterResponse: [
      async (request, options, response) => {
        // ログ記録
        console.log('レスポンス受信:', {
          url: request.url,
          status: response.status,
          duration: Date.now() - request.startTime
        });

        // 403エラーでトークンリフレッシュと再試行
        if (response.status === 403) {
          const newToken = await ky.post('/auth/refresh').text();
          localStorage.setItem('authToken', newToken);
          
          request.headers.set('Authorization', `Bearer ${newToken}`);
          return ky(request);
        }

        return response;
      }
    ],
    beforeRetry: [
      async ({ request, options, error, retryCount }) => {
        console.log(`リトライ ${retryCount}: ${error.message}`);
        
        // 特定条件でリトライ停止
        if (error.response?.status === 404) {
          throw new Error('リソースが存在しないためリトライを停止');
        }
      }
    ]
  }
});

// form-urlencodedデータの送信
async function submitForm(formData) {
  const params = new URLSearchParams();
  params.append('username', formData.username);
  params.append('password', formData.password);

  const response = await ky.post('https://api.example.com/login', {
    body: params // 自動的にapplication/x-www-form-urlencodedに設定
  });

  return await response.json();
}