Ky
モダンブラウザ、Node.js、Deno向けの軽量HTTPクライアント。Fetch APIをベースに構築されながら、より使いやすいAPIと拡張機能を提供。TypeScript完全サポート、自動リトライ、タイムアウト、JSON処理、hooks機能を内蔵。
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
スター履歴
データ取得日時: 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();
}