CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-ky

Tiny and elegant HTTP client based on the Fetch API

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

configuration.mddocs/

Configuration Options

Comprehensive options for customizing request behavior including JSON handling, URL manipulation, timeouts, progress tracking, and fetch customization.

Capabilities

JSON Options

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" };
    }
  }
});

URL and Search Parameters

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();

Timeout Configuration

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
});

Error Handling Configuration

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);
}

Progress Tracking

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;
};

Custom Fetch Implementation

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 });
  }
});

Complete Options Interface

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';

docs

configuration.md

errors.md

hooks.md

http-methods.md

index.md

instances.md

responses.md

retry.md

tile.json