Tiny and elegant HTTP client based on the Fetch API
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Comprehensive options for customizing request behavior including JSON handling, URL manipulation, timeouts, progress tracking, and fetch customization.
Configure how JSON data is handled in requests and responses.
interface JsonOptions {
/** Shortcut for sending JSON data in request body */
json?: unknown;
/** Custom JSON parsing function for responses */
parseJson?: (text: string) => unknown;
/** Custom JSON stringification function for requests */
stringifyJson?: (data: unknown) => string;
}Usage Examples:
import ky from "ky";
// Send JSON data
const user = await ky.post("https://api.example.com/users", {
json: { name: "Alice", email: "alice@example.com", age: 30 }
}).json();
// Custom JSON parsing with date handling
const client = ky.create({
parseJson: (text) => {
return JSON.parse(text, (key, value) => {
if (key.endsWith("_at") || key.endsWith("Date")) {
return new Date(value);
}
return value;
});
}
});
// Custom JSON stringification with BigInt support
const bigIntClient = ky.create({
stringifyJson: (data) => {
return JSON.stringify(data, (key, value) => {
if (typeof value === "bigint") {
return value.toString();
}
return value;
});
}
});
// Safe JSON parsing with error handling
const safeClient = ky.create({
parseJson: (text) => {
try {
return JSON.parse(text);
} catch (error) {
console.error("JSON parse error:", error);
return { error: "Invalid JSON response" };
}
}
});Configure URL prefixes and search parameters for requests.
interface UrlOptions {
/** URL prefix to prepend to requests */
prefixUrl?: URL | string;
/** Search parameters to include in request URL */
searchParams?: SearchParamsOption;
}
type SearchParamsInit = string | string[][] | Record<string, string> | URLSearchParams | undefined;
type SearchParamsOption =
| SearchParamsInit
| Record<string, string | number | boolean | undefined>
| Array<Array<string | number | boolean>>;Usage Examples:
import ky from "ky";
// API client with base URL
const apiClient = ky.create({
prefixUrl: "https://api.example.com/v1"
});
// All requests use the prefix
const users = await apiClient.get("users").json(); // GET https://api.example.com/v1/users
const user = await apiClient.get("users/123").json(); // GET https://api.example.com/v1/users/123
// Search parameters as object
const filteredData = await ky.get("https://api.example.com/data", {
searchParams: {
category: "electronics",
minPrice: 100,
maxPrice: 500,
inStock: true,
tags: undefined // Will be filtered out
}
}).json();
// Search parameters as URLSearchParams
const params = new URLSearchParams();
params.append("sort", "name");
params.append("sort", "date");
params.append("limit", "20");
const sortedData = await ky.get("https://api.example.com/items", {
searchParams: params
}).json();
// Search parameters as array
const complexParams = await ky.get("https://api.example.com/search", {
searchParams: [
["query", "javascript"],
["category", "programming"],
["category", "web"],
["format", "json"]
]
}).json();Configure request timeouts to prevent hanging requests.
interface TimeoutOptions {
/** Timeout in milliseconds, or false to disable */
timeout?: number | false;
}Usage Examples:
import ky from "ky";
// Global timeout for all requests
const fastClient = ky.create({
timeout: 5000 // 5 seconds
});
// Per-request timeout
const quickData = await ky.get("https://api.example.com/quick", {
timeout: 2000 // 2 seconds
}).json();
// Long timeout for slow operations
const largeFile = await ky.get("https://api.example.com/export", {
timeout: 60000 // 60 seconds
}).arrayBuffer();
// Disable timeout for streaming operations
const stream = await ky.get("https://api.example.com/stream", {
timeout: false
});
// Environment-specific timeouts
const timeoutClient = ky.create({
timeout: process.env.NODE_ENV === "development" ? 30000 : 10000
});Configure how HTTP errors are handled.
interface ErrorOptions {
/** Whether to throw HTTPError for non-2xx status codes */
throwHttpErrors?: boolean;
}Usage Examples:
import ky from "ky";
// Disable automatic error throwing
const response = await ky.get("https://api.example.com/might-fail", {
throwHttpErrors: false
});
if (response.ok) {
const data = await response.json();
console.log("Success:", data);
} else {
console.log("Failed with status:", response.status);
}
// Client that never throws HTTP errors
const tolerantClient = ky.create({
throwHttpErrors: false
});
// Handle errors manually
const result = await tolerantClient.get("https://api.example.com/data");
if (result.status === 404) {
console.log("Resource not found");
} else if (result.status >= 500) {
console.log("Server error");
} else if (result.ok) {
const data = await result.json();
console.log("Data:", data);
}Track upload and download progress for large requests.
interface ProgressOptions {
/** Download progress handler */
onDownloadProgress?: (progress: Progress, chunk: Uint8Array) => void;
/** Upload progress handler */
onUploadProgress?: (progress: Progress, chunk: Uint8Array) => void;
}
interface Progress {
/** Progress as decimal (0.0 to 1.0) */
percent: number;
/** Bytes transferred so far */
transferredBytes: number;
/** Total bytes (0 if unknown) */
totalBytes: number;
}Usage Examples:
import ky from "ky";
// Download progress tracking
const fileData = await ky.get("https://api.example.com/large-file.zip", {
onDownloadProgress: (progress, chunk) => {
const percentage = Math.round(progress.percent * 100);
console.log(`Download: ${percentage}% (${progress.transferredBytes}/${progress.totalBytes} bytes)`);
// Update progress bar
updateProgressBar(percentage);
}
}).arrayBuffer();
// Upload progress tracking
const formData = new FormData();
formData.append("file", largeFile);
const uploadResult = await ky.post("https://api.example.com/upload", {
body: formData,
onUploadProgress: (progress, chunk) => {
const percentage = Math.round(progress.percent * 100);
console.log(`Upload: ${percentage}% (${progress.transferredBytes}/${progress.totalBytes} bytes)`);
// Show upload status
setUploadStatus(`Uploading... ${percentage}%`);
}
}).json();
// Combined progress tracking
const processLargeFile = async () => {
const result = await ky.post("https://api.example.com/process", {
json: { fileUrl: "https://example.com/input.dat" },
onUploadProgress: (progress) => {
console.log(`Sending request: ${Math.round(progress.percent * 100)}%`);
},
onDownloadProgress: (progress) => {
console.log(`Receiving response: ${Math.round(progress.percent * 100)}%`);
}
}).json();
return result;
};Provide a custom fetch implementation for advanced use cases.
interface FetchOptions {
/** Custom fetch implementation */
fetch?: (input: Input, init?: RequestInit) => Promise<Response>;
}Usage Examples:
import ky from "ky";
// Use custom fetch with additional features
const customClient = ky.create({
fetch: async (input, init) => {
// Add request logging
console.log(`Fetching: ${init?.method || "GET"} ${input}`);
// Use native fetch with modifications
const response = await fetch(input, {
...init,
// Add custom headers
headers: {
...init?.headers,
"X-Custom-Client": "ky-enhanced"
}
});
// Add response logging
console.log(`Response: ${response.status} ${input}`);
return response;
}
});
// Node.js with undici or node-fetch
import { fetch } from "undici";
const nodeClient = ky.create({
fetch: fetch as any
});
// Mock fetch for testing
const mockClient = ky.create({
fetch: async (input, init) => {
const url = input.toString();
if (url.includes("/users")) {
return new Response(JSON.stringify([
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" }
]), {
status: 200,
headers: { "Content-Type": "application/json" }
});
}
return new Response("Not found", { status: 404 });
}
});interface Options extends KyOptions, RequestInit {
method?: LiteralUnion<HttpMethod, string>;
headers?: KyHeadersInit;
}
interface KyOptions {
json?: unknown;
parseJson?: (text: string) => unknown;
stringifyJson?: (data: unknown) => string;
searchParams?: SearchParamsOption;
prefixUrl?: URL | string;
retry?: RetryOptions | number;
timeout?: number | false;
throwHttpErrors?: boolean;
onDownloadProgress?: (progress: Progress, chunk: Uint8Array) => void;
onUploadProgress?: (progress: Progress, chunk: Uint8Array) => void;
fetch?: (input: Input, init?: RequestInit) => Promise<Response>;
hooks?: Hooks;
}
type KyHeadersInit = NonNullable<RequestInit['headers']> | Record<string, string | undefined>;
type LiteralUnion<T extends U, U = string> = T | (U & Record<never, never>);
type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'head' | 'delete';