Ky
Lightweight HTTP client with complete TypeScript support. Built on Fetch API while providing more user-friendly API and extended features. Built-in strict type definitions, auto-retry, timeouts, JSON processing, and hooks functionality. Optimized for modern JavaScript/TypeScript environments.
Library
Ky
Overview
Ky is "a lightweight HTTP client for modern browsers, Node.js, and Deno" built on the Fetch API while providing a more user-friendly API and extended features. With complete TypeScript support, built-in auto-retry, timeouts, JSON processing, and hooks functionality, it has gained attention as a lightweight alternative to Axios. Its Promise-based and chainable API enables intuitive HTTP request processing in modern JavaScript development.
Details
Ky 2025 is rapidly gaining attention as an improved version of the Fetch API for HTTP client libraries. Highly valued especially by TypeScript developers and modern JavaScript environments, it achieves a balance between bundle size and developer experience. In addition to the standard Fetch API, it provides auto-retry, error handling, progress tracking, and request/response hook functionality. The unified API across browsers, Node.js, and Deno supports universal JavaScript application development.
Key Features
- Fetch API Foundation: Lightweight design based on modern browser standard APIs
- Complete TypeScript Support: Type-safe API design and development-time completion features
- Auto-retry: Configurable exponential backoff retry functionality
- Hooks System: Intervention capability for request/response lifecycle
- Progress Tracking: Real-time monitoring of upload/download progress
- Unified API: Consistent interface across browsers, Node.js, and Deno
Pros and Cons
Pros
- Lightweight and high performance in modern browsers through Fetch API foundation
- Improved type safety and development efficiency through complete TypeScript support
- Robust network processing through auto-retry and error handling
- Flexible request/response processing customization through Hooks system
- Readable code writing through intuitive chainable API
- Performance improvement through smaller bundle size compared to Axios
Cons
- Unavailable in older browsers (without Fetch API support)
- Requires additional polyfills in Node.js versions below 18
- Limited feature set compared to Axios
- Ecosystem and plugins not as rich as Axios
- Fewer learning resources and examples than Axios
- Some advanced request configuration features are limited
Reference Pages
Code Examples
Installation and Basic Setup
# Install Ky
npm install ky
# Usage with Deno (URL import)
# import ky from 'https://esm.sh/ky';
# TypeScript type definitions are automatically included
# @types/ky not required
Basic Requests (GET/POST/PUT/DELETE)
// ES6 modules import
import ky from 'ky';
// Basic GET request
const data = await ky.get('https://api.example.com/users').json();
console.log('User data:', data);
// GET request with TypeScript type specification
interface User {
id: number;
name: string;
email: string;
}
const users = await ky.get<User[]>('https://api.example.com/users').json();
console.log('Type-safe user data:', users);
// POST request (sending JSON)
const newUser = await ky.post('https://api.example.com/users', {
json: {
name: 'John Doe',
email: '[email protected]'
}
}).json();
console.log('Created user:', newUser);
// PUT request (update)
const updatedUser = await ky.put('https://api.example.com/users/123', {
json: {
name: 'Jane Doe',
email: '[email protected]'
}
}).json();
// DELETE request
await ky.delete('https://api.example.com/users/123');
console.log('User deleted');
// Getting various response formats
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();
// Leveraging chainable API
const response = await ky
.get('https://api.example.com/data')
.json();
// Status code and header verification
const httpResponse = await ky.get('https://api.example.com/status');
console.log('Status:', httpResponse.status);
console.log('Headers:', Object.fromEntries(httpResponse.headers));
Advanced Configuration and Customization (Headers, Authentication, Timeout, etc.)
// Custom headers and authentication settings
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();
// Timeout configuration
try {
const data = await ky.get('https://api.example.com/slow-endpoint', {
timeout: 5000 // 5 second timeout
}).json();
} catch (error) {
if (error.name === 'TimeoutError') {
console.log('Request timed out');
}
}
// Search parameters (query parameters) configuration
const searchData = await ky.get('https://api.example.com/search', {
searchParams: {
q: 'JavaScript',
page: 1,
limit: 10,
sort: 'created_at'
}
}).json();
// Query parameters using 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();
// Base URL configuration using prefixUrl
const api = ky.create({
prefixUrl: 'https://api.example.com/v1'
});
const user = await api.get('users/123').json();
// Actual URL: https://api.example.com/v1/users/123
// Request cancellation using AbortController
const controller = new AbortController();
setTimeout(() => {
controller.abort();
}, 3000); // Cancel after 3 seconds
try {
const data = await ky.get('https://api.example.com/data', {
signal: controller.signal
}).json();
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request was cancelled');
}
}
// Custom Content-Type configuration
const response = await ky.post('https://api.example.com/data', {
headers: {
'Content-Type': 'application/vnd.api+json'
},
json: {
data: {
type: 'articles',
attributes: {
title: 'Custom Content Type Example'
}
}
}
});
Error Handling and Retry Functionality
// Detailed error handling
try {
const data = await ky.get('https://api.example.com/users').json();
} catch (error) {
if (error.name === 'HTTPError') {
console.log('HTTP Error:', error.response.status);
console.log('Response:', await error.response.text());
// Status code-specific processing
switch (error.response.status) {
case 400:
console.error('Invalid request');
break;
case 401:
console.error('Authentication required');
break;
case 404:
console.error('Resource not found');
break;
case 500:
console.error('Server error');
break;
}
} else if (error.name === 'TimeoutError') {
console.error('Request timed out');
} else {
console.error('Network error:', error.message);
}
}
// Custom retry configuration
const data = await ky.get('https://api.example.com/unstable-endpoint', {
retry: {
limit: 5, // Maximum 5 retries
methods: ['get', 'post'], // Retry for GET and POST methods
statusCodes: [408, 413, 429, 500, 502, 503, 504], // Retry target statuses
backoffLimit: 3000, // Maximum delay time (3 seconds)
delay: attemptCount => Math.min(1000 * (2 ** attemptCount), 3000) // Custom delay function
}
}).json();
// Simple retry limit configuration
const simpleRetryData = await ky.get('https://api.example.com/data', {
retry: 3 // Retry up to 3 times
}).json();
// Error customization with beforeError hook
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 Error: ${response.status} - ${response.statusText}`;
}
return error;
}
]
}
}).json();
// Get response without throwing errors
const responseWithoutThrow = await ky.get('https://api.example.com/maybe-error', {
throwHttpErrors: false
});
if (!responseWithoutThrow.ok) {
console.log('Error response:', responseWithoutThrow.status);
const errorData = await responseWithoutThrow.json();
console.log('Error details:', errorData);
} else {
const successData = await responseWithoutThrow.json();
console.log('Success data:', successData);
}
Concurrent Processing and Asynchronous Requests
// Parallel execution of multiple requests
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:', users);
console.log('Posts:', posts);
console.log('Comments:', comments);
return { users, posts, comments };
} catch (error) {
console.error('Parallel request error:', error);
throw error;
}
}
// Partial failure tolerance pattern using 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 requests:', successful);
console.log('Failed requests:', failed);
return { successful, failed };
}
// Pagination-aware progressive data fetching
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} completed: ${pageData.items.length} items`);
// Wait to reduce API load
if (hasMore) {
await new Promise(resolve => setTimeout(resolve, 100));
}
} catch (error) {
console.error(`Error on page ${page}:`, error);
break;
}
}
console.log(`Total ${allData.length} items fetched`);
return allData;
}
// Conditional request execution
async function conditionalRequests() {
const results = await Promise.allSettled([
ky.get('https://api.example.com/endpoint1').json(),
ky.get('https://api.example.com/endpoint2').json()
]);
// Additional processing only if first request succeeds
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: 'Main request failed' };
}
Framework Integration and Practical Examples
// React Hooks integration example
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 };
}
// Usage in React component
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>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!user) return <div>User not found</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
// Creating and extending API client instances
const apiClient = ky.create({
prefixUrl: 'https://api.example.com/v1',
headers: {
'Accept': 'application/json'
},
timeout: 10000,
retry: 2
});
// Authenticated API client
const authenticatedApi = apiClient.extend({
hooks: {
beforeRequest: [
request => {
const token = localStorage.getItem('authToken');
if (token) {
request.headers.set('Authorization', `Bearer ${token}`);
}
}
]
}
});
// File upload (FormData)
async function uploadFile(file, metadata = {}) {
const formData = new FormData();
formData.append('file', file);
// Add metadata
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(`Upload progress: ${percent}%`);
console.log(`Transferred: ${progress.transferredBytes}/${progress.totalBytes} bytes`);
}
});
return await response.json();
} catch (error) {
console.error('Upload error:', error);
throw error;
}
}
// Download progress tracking
async function downloadFile(url, filename) {
try {
const response = await ky.get(url, {
onDownloadProgress: (progress, chunk) => {
const percent = Math.round(progress.percent * 100);
console.log(`Download progress: ${percent}%`);
updateProgressBar(percent);
}
});
const blob = await response.blob();
// Browser file download
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('Download completed');
} catch (error) {
console.error('Download error:', error);
throw error;
}
}
// Advanced processing using Hooks
const advancedApi = ky.create({
prefixUrl: 'https://api.example.com',
hooks: {
beforeRequest: [
request => {
// Common pre-request processing
request.headers.set('X-Request-ID', generateRequestId());
console.log('Sending request:', request.url);
}
],
afterResponse: [
async (request, options, response) => {
// Logging
console.log('Response received:', {
url: request.url,
status: response.status,
duration: Date.now() - request.startTime
});
// Token refresh and retry on 403 error
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(`Retry ${retryCount}: ${error.message}`);
// Stop retry under specific conditions
if (error.response?.status === 404) {
throw new Error('Stopping retry because resource does not exist');
}
}
]
}
});
// Sending form-urlencoded data
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 // Automatically set to application/x-www-form-urlencoded
});
return await response.json();
}