@fgrzl/fetch
Version:
A modern, type-safe HTTP client with middleware support for CSRF protection and authentication
1,598 lines (1,555 loc) • 50.1 kB
TypeScript
/**
* @fileoverview Type definitions for the HTTP client.
*
* This file contains core TypeScript interfaces and types for FetchClient.
* Designed for discoverability and type safety.
*/
/**
* Typed response wrapper with consistent shape.
*
* ✅ Always returns this shape (never throws)
* ✅ Use `.ok` to check success
* ✅ Use `.data` for parsed response
* ✅ Use `.error` for failure details
*
* @template T - The expected type of the response data
*
* @example
* ```typescript
* const result = await client.get<User[]>('/api/users');
* if (result.ok) {
* console.log(result.data); // Type is User[]
* } else {
* console.error(result.error?.message); // Handle error
* }
* ```
*/
interface FetchResponse<T> {
/** The parsed response data (null if request failed) */
data: T | null;
/** HTTP status code (0 for network errors) */
status: number;
/** HTTP status text ('Network Error' for network failures) */
statusText: string;
/** Response headers */
headers: Headers;
/** The request URL */
url: string;
/** True if status 200-299, false otherwise */
ok: boolean;
/** Error details when ok is false */
error?: {
/** Human-readable error message */
message: string;
/** Raw error response body */
body?: unknown;
};
}
/**
* Configuration options for FetchClient.
*
* Optimized for "pit of success" - good defaults, minimal required config.
*/
interface FetchClientOptions {
/**
* Controls credential handling for requests.
*
* - 'same-origin' (default): Send cookies for same-origin requests
* - 'include': Always send cookies
* - 'omit': Never send cookies
*/
credentials?: RequestCredentials;
/**
* Base URL for relative requests.
*
* When set, all relative URLs (not starting with http:// or https://) will be
* prefixed with this base URL. Absolute URLs are used as-is.
*
* @example
* ```typescript
* const client = new FetchClient({ baseUrl: 'https://api.example.com' });
* await client.get('/users'); // → GET https://api.example.com/users
* await client.get('https://other-api.com/data'); // → GET https://other-api.com/data
* ```
*/
baseUrl?: string;
}
/**
* @fileoverview Enhanced fetch client with intercept middleware architecture.
*/
/**
* Intercept middleware type that allows full control over request/response cycle.
* Middleware can modify requests, handle responses, implement retries, etc.
*/
type FetchMiddleware = (request: RequestInit & {
url?: string;
}, next: (modifiedRequest?: RequestInit & {
url?: string;
}) => Promise<FetchResponse<unknown>>) => Promise<FetchResponse<unknown>>;
/**
* Enhanced HTTP client with intercept middleware architecture.
*
* Features:
* - 🎯 Smart defaults (JSON content-type, same-origin credentials)
* - 🔧 Powerful middleware system for cross-cutting concerns
* - 🛡️ Consistent error handling (never throws, always returns response)
* - 📦 TypeScript-first with full type inference
* - 🚀 Modern async/await API
*
* @example Basic usage:
* ```typescript
* const client = new FetchClient();
*
* // GET request - just works
* const users = await client.get<User[]>('/api/users');
* if (users.ok) {
* console.log(users.data); // Type is User[]
* }
*
* // POST request - JSON by default
* const result = await client.post('/api/users', { name: 'John' });
* ```
*
* @example With middleware:
* ```typescript
* const client = new FetchClient();
*
* // Add auth middleware
* client.use((request, next) => {
* request.headers = { ...request.headers, Authorization: 'Bearer token' };
* return next(request);
* });
*
* // Now all requests include auth
* const data = await client.get('/api/protected');
* ```
*/
declare class FetchClient {
private middlewares;
private credentials;
private baseUrl;
constructor(config?: FetchClientOptions);
use(middleware: FetchMiddleware): this;
/**
* Set or update the base URL for this client instance.
*
* When a base URL is set, relative URLs will be resolved against it.
* Absolute URLs will continue to work unchanged.
*
* @param baseUrl - The base URL to set, or undefined to clear it
* @returns The client instance for method chaining
*
* @example Set base URL:
* ```typescript
* const client = new FetchClient();
* client.setBaseUrl('https://api.example.com');
*
* // Now relative URLs work
* await client.get('/users'); // → GET https://api.example.com/users
* ```
*
* @example Chain with middleware:
* ```typescript
* const client = useProductionStack(new FetchClient())
* .setBaseUrl(process.env.API_BASE_URL);
* ```
*/
setBaseUrl(baseUrl?: string): this;
request<T = unknown>(url: string, init?: RequestInit): Promise<FetchResponse<T>>;
private coreFetch;
private parseResponse;
private buildUrlWithParams;
/**
* Resolves a URL with the base URL if it's relative and base URL is configured
* @param url - The URL to resolve
* @returns The resolved URL
* @private
*/
private resolveUrl;
/**
* HEAD request with query parameter support.
*
* HEAD requests are used to retrieve metadata about a resource without downloading
* the response body. Useful for checking if a resource exists, getting content length,
* last modified date, etc.
*
* @template T - Expected response data type (will be null for HEAD requests)
* @param url - Request URL
* @param params - Query parameters to append to URL
* @returns Promise resolving to typed response (data will always be null)
*
* @example Check if resource exists:
* ```typescript
* const headResponse = await client.head('/api/large-file.zip');
* if (headResponse.ok) {
* const contentLength = headResponse.headers.get('content-length');
* const lastModified = headResponse.headers.get('last-modified');
* console.log(`File size: ${contentLength} bytes`);
* }
* ```
*
* @example Check with query parameters:
* ```typescript
* const exists = await client.head('/api/users', { id: 123 });
* if (exists.status === 404) {
* console.log('User not found');
* }
* ```
*/
head<T = null>(url: string, params?: Record<string, string | number | boolean | undefined>): Promise<FetchResponse<T>>;
/**
* HEAD request that returns useful metadata about a resource.
*
* This is a convenience method that extracts common metadata from HEAD responses
* for easier consumption.
*
* @param url - Request URL
* @param params - Query parameters to append to URL
* @returns Promise resolving to response with extracted metadata
*
* @example Get resource metadata:
* ```typescript
* const metadata = await client.headMetadata('/api/large-file.zip');
* if (metadata.ok) {
* console.log('File exists:', metadata.exists);
* console.log('Content type:', metadata.contentType);
* console.log('Size:', metadata.contentLength, 'bytes');
* console.log('Last modified:', metadata.lastModified);
* }
* ```
*/
headMetadata(url: string, params?: Record<string, string | number | boolean | undefined>): Promise<FetchResponse<null> & {
exists: boolean;
contentType: string | undefined;
contentLength: number | undefined;
lastModified: Date | undefined;
etag: string | undefined;
cacheControl: string | undefined;
}>;
/**
* GET request with query parameter support.
*
* @template T - Expected response data type
* @param url - Request URL
* @param params - Query parameters to append to URL
* @returns Promise resolving to typed response
*
* @example
* ```typescript
* const users = await client.get<User[]>('/api/users');
* const filteredUsers = await client.get<User[]>('/api/users', { status: 'active', limit: 10 });
* if (users.ok) console.log(users.data);
* ```
*/
get<T>(url: string, params?: Record<string, string | number | boolean | undefined>): Promise<FetchResponse<T>>;
/**
* POST request with automatic JSON serialization.
*
* @template T - Expected response data type
* @param url - Request URL
* @param body - Request body (auto-serialized to JSON)
* @param headers - Additional headers (Content-Type: application/json is default)
* @returns Promise resolving to typed response
*
* @example
* ```typescript
* const result = await client.post<User>('/api/users', { name: 'John' });
* ```
*/
post<T>(url: string, body?: unknown, headers?: Record<string, string>): Promise<FetchResponse<T>>;
/**
* PUT request with automatic JSON serialization.
*
* @template T - Expected response data type
* @param url - Request URL
* @param body - Request body (auto-serialized to JSON)
* @param headers - Additional headers (Content-Type: application/json is default)
* @returns Promise resolving to typed response
*/
put<T>(url: string, body?: unknown, headers?: Record<string, string>): Promise<FetchResponse<T>>;
/**
* PATCH request with automatic JSON serialization.
*
* @template T - Expected response data type
* @param url - Request URL
* @param body - Request body (auto-serialized to JSON)
* @param headers - Additional headers (Content-Type: application/json is default)
* @returns Promise resolving to typed response
*/
patch<T>(url: string, body?: unknown, headers?: Record<string, string>): Promise<FetchResponse<T>>;
/**
* DELETE request with query parameter support.
*
* @template T - Expected response data type
* @param url - Request URL
* @param params - Query parameters to append to URL
* @returns Promise resolving to typed response
*
* @example
* ```typescript
* const result = await client.del('/api/users/123');
* const bulkResult = await client.del('/api/users', { status: 'inactive', force: true });
* if (result.ok) console.log('Deleted successfully');
* ```
*/
del<T>(url: string, params?: Record<string, string | number | boolean | undefined>): Promise<FetchResponse<T>>;
}
/**
* @fileoverview Custom error classes - "Pit of Success" pattern.
*
* 🎯 LEVEL 1: HttpError, NetworkError - Most common error types you'll catch
* 🎯 LEVEL 2: FetchError - Base error class for advanced error handling
*
* @example
* ```typescript
* try {
* await client.get('/api/data');
* } catch (error) {
* if (error instanceof HttpError) {
* console.log(`HTTP ${error.status}: ${error.statusText}`);
* } else if (error instanceof NetworkError) {
* console.log('Network connection failed');
* }
* }
* ```
*/
/**
* Base error class for all fetch client errors.
*/
declare class FetchError extends Error {
/** Optional underlying cause */
readonly cause?: Error;
/**
* Creates a new FetchError.
* @param message - Error message
* @param cause - Optional underlying cause
*/
constructor(message: string, cause?: Error);
}
/**
* Error thrown when an HTTP request fails with a non-2xx status code.
*/
declare class HttpError extends FetchError {
/** The HTTP status code */
readonly status: number;
/** The HTTP status text */
readonly statusText: string;
/** The response body (if available) */
readonly body: unknown;
/**
* Creates a new HttpError.
* @param status - HTTP status code
* @param statusText - HTTP status text
* @param body - Response body
* @param url - The request URL
*/
constructor(status: number, statusText: string, body: unknown, url: string);
}
/**
* Error thrown when a network request fails completely.
*/
declare class NetworkError extends FetchError {
/**
* Creates a new NetworkError.
* @param message - Error message
* @param url - The request URL
* @param cause - The underlying network error
*/
constructor(message: string, url: string, cause?: Error);
}
/**
* @fileoverview Query parameter utilities for FetchClient
*
* Provides utilities for building URL query strings from JavaScript objects
* with proper handling of arrays, undefined values, and URL encoding.
*/
/**
* Query parameter value types that can be converted to URL parameters.
* Arrays are handled specially with multiple parameter entries.
*/
type QueryValue = string | number | boolean | null | undefined | QueryValue[];
/**
* Object representing query parameters for URL construction.
*/
type QueryParams = Record<string, QueryValue>;
/**
* Builds a URL query string from a JavaScript object.
*
* Features:
* - ✅ Proper URL encoding via native URLSearchParams
* - ✅ Array handling with multiple parameter entries
* - ✅ Undefined value filtering (excluded from output)
* - ✅ Type coercion to strings for all values
*
* @param query - Object containing query parameters
* @returns URL-encoded query string (without leading '?')
*
* @example Basic usage:
* ```typescript
* buildQueryParams({ name: 'John', age: 30 })
* // → "name=John&age=30"
* ```
*
* @example Array handling:
* ```typescript
* buildQueryParams({ tags: ['typescript', 'javascript'], active: true })
* // → "tags=typescript&tags=javascript&active=true"
* ```
*
* @example Undefined filtering:
* ```typescript
* buildQueryParams({ name: 'John', email: undefined, age: null })
* // → "name=John&age=null" (undefined excluded, null converted to string)
* ```
*
* @example Real-world API usage:
* ```typescript
* const filters = {
* status: ['active', 'pending'],
* limit: 50,
* offset: 0,
* search: searchTerm || undefined // Conditionally included
* };
*
* const queryString = buildQueryParams(filters);
* // → "status=active&status=pending&limit=50&offset=0"
* // (search excluded if searchTerm is undefined)
* ```
*/
declare function buildQueryParams(query: QueryParams): string;
/**
* Appends query parameters to a URL, handling existing query strings properly.
*
* @param baseUrl - The base URL to append parameters to
* @param query - Object containing query parameters
* @returns Complete URL with query parameters appended
*
* @example Basic URL building:
* ```typescript
* appendQueryParams('/api/users', { limit: 10, active: true })
* // → "/api/users?limit=10&active=true"
* ```
*
* @example Existing query parameters:
* ```typescript
* appendQueryParams('/api/users?sort=name', { limit: 10 })
* // → "/api/users?sort=name&limit=10"
* ```
*
* @example Empty query object:
* ```typescript
* appendQueryParams('/api/users', {})
* // → "/api/users" (no change)
* ```
*/
declare function appendQueryParams(baseUrl: string, query: QueryParams): string;
/**
* @fileoverview Authentication middleware types and configuration.
*/
/**
* Authentication token provider function.
* Should return the current auth token or empty string if not available.
*/
type AuthTokenProvider = () => string | Promise<string>;
/**
* Authentication configuration options - optimized for "pit of success".
*
* Smart defaults:
* - Uses standard Authorization header with Bearer token
* - Applies to all requests by default
* - Graceful handling when token is unavailable
*/
interface AuthenticationOptions {
/**
* Function to get the current authentication token.
* Can be synchronous or asynchronous.
*
* @returns The auth token or empty string if not available
*
* @example Token from localStorage:
* ```typescript
* const getToken = () => localStorage.getItem('auth-token') || '';
* ```
*
* @example Async token refresh:
* ```typescript
* const getToken = async () => {
* const token = localStorage.getItem('auth-token');
* if (!token || isExpired(token)) {
* return await refreshToken();
* }
* return token;
* };
* ```
*/
tokenProvider: AuthTokenProvider;
/**
* Header name for the authentication token (default: 'Authorization')
* The token will be prefixed with the tokenType
*/
headerName?: string;
/**
* Token type prefix (default: 'Bearer')
* Common alternatives: 'Token', 'JWT', 'ApiKey'
*/
tokenType?: string;
/**
* Skip authentication for requests matching these URL patterns
* Useful for public endpoints that don't need auth
*
* @example
* ```typescript
* skipPatterns: [/^\/public\//, '/health', '/login']
* ```
*/
skipPatterns?: (RegExp | string)[];
/**
* Only apply authentication to requests matching these patterns
* If specified, only these patterns will get auth headers
*
* @example
* ```typescript
* includePatterns: [/^\/api\//, '/graphql']
* ```
*/
includePatterns?: (RegExp | string)[];
}
/**
* @fileoverview Authentication middleware implementation.
*/
/**
* Creates authentication middleware with smart defaults.
* Automatically adds Bearer tokens to requests.
*
* @param options - Authentication configuration options
* @returns Authentication middleware for use with FetchClient
*
* @example Basic usage:
* ```typescript
* const authClient = useAuthentication(client, {
* tokenProvider: () => localStorage.getItem('token') || ''
* });
* ```
*
* @example Async token provider:
* ```typescript
* const authClient = useAuthentication(client, {
* tokenProvider: async () => {
* const token = await getAuthToken();
* return token || '';
* }
* });
* ```
*/
declare function createAuthenticationMiddleware(options: AuthenticationOptions): FetchMiddleware;
/**
* @fileoverview Authentication middleware - "pit of success" API.
*/
/**
* "Pit of success" API for adding authentication to a FetchClient.
* Automatically adds Bearer tokens to requests.
*
* @param client - The FetchClient to add authentication to
* @param options - Authentication configuration
* @returns A new FetchClient with authentication middleware
*
* @example Basic token from localStorage:
* ```typescript
* const authClient = useAuthentication(client, {
* tokenProvider: () => localStorage.getItem('auth-token') || ''
* });
* ```
*
* @example Async token with refresh:
* ```typescript
* const authClient = useAuthentication(client, {
* tokenProvider: async () => {
* let token = localStorage.getItem('auth-token');
* if (!token || isExpired(token)) {
* token = await refreshToken();
* }
* return token || '';
* }
* });
* ```
*/
declare function useAuthentication(client: FetchClient, options: AuthenticationOptions): FetchClient;
/**
* @fileoverview Authorization middleware types and configuration.
*/
/**
* Handler function for unauthorized/forbidden responses.
*/
type UnauthorizedHandler = (response: FetchResponse<unknown>, request: RequestInit & {
url?: string;
}) => void | Promise<void>;
/**
* Smart default configuration for redirect-based authorization handling.
*/
interface RedirectAuthorizationConfig {
/**
* Path to redirect to on unauthorized response (default: '/login')
*/
redirectPath?: string;
/**
* Query parameter name for the return URL (default: 'return_url')
*/
returnUrlParam?: string;
/**
* Whether to include the current URL as a return URL (default: true)
* Set to false if you don't want the return URL functionality
*/
includeReturnUrl?: boolean;
}
/**
* Authorization configuration options - optimized for "pit of success".
*
* Smart defaults:
* - Handles 401 Unauthorized responses
* - Optionally handles 403 Forbidden responses
* - Graceful error handling
* - When no options provided, defaults to redirecting to /login with return URL
*/
interface AuthorizationOptions {
/**
* Handler called when 401 Unauthorized response is received.
*
* @param response - The 401 response object
* @param request - The original request that was unauthorized
*
* @example Redirect to login:
* ```typescript
* onUnauthorized: () => {
* window.location.href = '/login';
* }
* ```
*
* @example Clear token and reload:
* ```typescript
* onUnauthorized: () => {
* localStorage.removeItem('auth-token');
* window.location.reload();
* }
* ```
*/
onUnauthorized?: UnauthorizedHandler;
/**
* Smart default configuration for redirect-based authorization.
* When provided, creates a default onUnauthorized handler that redirects
* to the login page with a return URL.
*
* @example Use defaults (redirects to '/login?return_url=current-page'):
* ```typescript
* redirectConfig: {}
* ```
*
* @example Custom redirect path:
* ```typescript
* redirectConfig: { redirectPath: '/signin' }
* ```
*/
redirectConfig?: RedirectAuthorizationConfig;
/**
* Handler called when 403 Forbidden response is received.
* Optional - if not provided, 403 responses are ignored.
*
* @param response - The 403 response object
* @param request - The original request that was forbidden
*/
onForbidden?: UnauthorizedHandler;
/**
* Skip authorization handling for requests matching these URL patterns
* Useful for login/public endpoints where 401 is expected
*
* @example
* ```typescript
* skipPatterns: ['/login', '/register', /^\/public\//]
* ```
*/
skipPatterns?: (RegExp | string)[];
/**
* Status codes to handle (default: [401])
* You can add 403 if you want to handle forbidden responses
*/
statusCodes?: number[];
}
/**
* @fileoverview Authorization middleware implementation.
*/
/**
* Creates authorization middleware with smart defaults.
* Handles 401/403 responses by calling configured handlers.
*
* @param options - Authorization configuration options (optional)
* @returns Authorization middleware for use with FetchClient
*
* @example Smart defaults (no configuration needed):
* ```typescript
* const authzClient = useAuthorization(client);
* // Redirects to '/login?return_url=current-page' on 401
* ```
*
* @example Custom redirect configuration:
* ```typescript
* const authzClient = useAuthorization(client, {
* redirectConfig: {
* redirectPath: '/signin',
* returnUrlParam: 'redirect_to'
* }
* });
* ```
*
* @example Manual handler (full control):
* ```typescript
* const authzClient = useAuthorization(client, {
* onUnauthorized: () => window.location.href = '/login'
* });
* ```
*
* @example Handle both 401 and 403:
* ```typescript
* const authzClient = useAuthorization(client, {
* onForbidden: () => showAccessDeniedMessage(),
* statusCodes: [401, 403]
* });
* ```
*/
declare function createAuthorizationMiddleware(options?: AuthorizationOptions): FetchMiddleware;
/**
* @fileoverview Authorization middleware - "pit of success" API.
*/
/**
* "Pit of success" API for adding authorization handling to a FetchClient.
* Automatically handles 401 Unauthorized responses.
*
* @param client - The FetchClient to add authorization handling to
* @param options - Authorization configuration (optional)
* @returns A new FetchClient with authorization middleware
*
* @example Smart defaults - no configuration needed:
* ```typescript
* const authzClient = useAuthorization(client);
* // Redirects to '/login?return_url=current-page' on 401
* ```
*
* @example Custom redirect path:
* ```typescript
* const authzClient = useAuthorization(client, {
* redirectConfig: { redirectPath: '/signin', returnUrlParam: 'redirect_to' }
* });
* ```
*
* @example Manual handler (full control):
* ```typescript
* const authzClient = useAuthorization(client, {
* onUnauthorized: () => {
* localStorage.removeItem('auth-token');
* window.location.href = '/login';
* }
* });
* ```
*
* @example Handle multiple status codes:
* ```typescript
* const authzClient = useAuthorization(client, {
* onForbidden: () => showAccessDenied(),
* statusCodes: [401, 403]
* });
* ```
*/
declare function useAuthorization(client: FetchClient, options?: AuthorizationOptions): FetchClient;
/**
* @fileoverview Cache middleware types and configuration.
*/
/**
* Cache key generator function.
* Should return a unique key for the request.
*/
type CacheKeyGenerator = (request: RequestInit & {
url?: string;
}) => string;
/**
* Cache storage interface.
* Allows custom cache implementations.
*/
interface CacheStorage {
get(key: string): Promise<CacheEntry | null>;
getWithExpiry?(key: string): Promise<{
entry: CacheEntry | null;
isExpired: boolean;
}>;
set(key: string, entry: CacheEntry): Promise<void>;
delete(key: string): Promise<void>;
clear(): Promise<void>;
}
/**
* Cached response entry.
*/
interface CacheEntry {
response: {
status: number;
statusText: string;
headers: Record<string, string>;
data: unknown;
};
timestamp: number;
expiresAt: number;
}
/**
* Cache configuration options - optimized for "pit of success".
*
* Smart defaults:
* - Only caches GET requests
* - 5 minute default TTL
* - Memory-based storage
* - Automatic cache key generation
*/
interface CacheOptions {
/**
* Time to live in milliseconds (default: 300000 = 5 minutes)
* How long responses should be cached
*/
ttl?: number;
/**
* HTTP methods to cache (default: ['GET'])
* Only these methods will be cached
*/
methods?: string[];
/**
* Cache storage implementation (default: in-memory)
* Can be replaced with localStorage, IndexedDB, etc.
*/
storage?: CacheStorage;
/**
* Custom cache key generator (default: URL + method + headers)
* Should return a unique key for each request
*
* @example Custom key generator:
* ```typescript
* keyGenerator: (request) => `${request.method}:${request.url}`
* ```
*/
keyGenerator?: CacheKeyGenerator;
/**
* Skip caching for requests matching these URL patterns
*
* @example
* ```typescript
* skipPatterns: [/\/api\/user/, '/dynamic-data']
* ```
*/
skipPatterns?: (RegExp | string)[];
/**
* Whether to serve stale cache entries while revalidating
* When true, returns cached data immediately and updates cache in background
*/
staleWhileRevalidate?: boolean;
}
/**
* @fileoverview Cache middleware implementation.
*/
/**
* Creates cache middleware with smart defaults.
* Caches GET responses for faster subsequent requests.
*
* @param options - Cache configuration options
* @returns Cache middleware for use with FetchClient
*
* @example Basic caching:
* ```typescript
* const cachedClient = useCache(client);
* // GET requests will be cached for 5 minutes
* ```
*
* @example Custom TTL:
* ```typescript
* const cachedClient = useCache(client, {
* ttl: 10 * 60 * 1000 // 10 minutes
* });
* ```
*/
declare function createCacheMiddleware(options?: CacheOptions): FetchMiddleware;
/**
* @fileoverview Cache middleware - "pit of success" API.
*/
/**
* "Pit of success" API for adding response caching to a FetchClient.
* Caches GET responses for faster subsequent requests.
*
* @param client - The FetchClient to add caching to
* @param options - Cache configuration options
* @returns A new FetchClient with cache middleware
*
* @example Basic caching (5 minute TTL):
* ```typescript
* const cachedClient = useCache(client);
*
* // First call hits the network
* await cachedClient.get('/api/data');
*
* // Second call returns cached data
* await cachedClient.get('/api/data');
* ```
*
* @example Custom TTL and methods:
* ```typescript
* const cachedClient = useCache(client, {
* ttl: 10 * 60 * 1000, // 10 minutes
* methods: ['GET', 'HEAD']
* });
* ```
*
* @example Stale-while-revalidate:
* ```typescript
* const cachedClient = useCache(client, {
* staleWhileRevalidate: true
* });
* // Returns stale data immediately, updates cache in background
* ```
*/
declare function useCache(client: FetchClient, options?: CacheOptions): FetchClient;
/**
* @fileoverview CSRF protection middleware types and configuration.
*/
/**
* CSRF token provider function.
* Should return the current CSRF token or empty string if not available.
*/
type CSRFTokenProvider = () => string;
/**
* CSRF configuration options - optimized for "pit of success".
*
* Smart defaults:
* - Uses standard X-XSRF-TOKEN header
* - Automatically extracts token from XSRF-TOKEN cookie
* - Only adds token to state-changing methods (POST, PUT, PATCH, DELETE)
*/
interface CSRFOptions {
/**
* Function to get the current CSRF token.
* Default: extracts from XSRF-TOKEN cookie (standard Rails/Laravel convention)
*
* @returns The CSRF token or empty string if not available
*
* @example Custom token provider:
* ```typescript
* const getToken = () => localStorage.getItem('csrf-token') || '';
* ```
*/
tokenProvider?: CSRFTokenProvider;
/**
* Header name to use for CSRF token (default: 'X-XSRF-TOKEN')
* Common alternatives: 'X-CSRF-Token', 'X-CSRFToken'
*/
headerName?: string;
/**
* Cookie name to read token from when using default provider (default: 'XSRF-TOKEN')
* Common alternatives: 'csrf-token', '_token'
*/
cookieName?: string;
/**
* HTTP methods that require CSRF protection (default: ['POST', 'PUT', 'PATCH', 'DELETE'])
* GET and HEAD requests typically don't need CSRF tokens
*/
protectedMethods?: string[];
/**
* Skip CSRF protection for requests matching these URL patterns
* Useful for external API calls that don't need CSRF tokens
*
* @example
* ```typescript
* skipPatterns: [/^https:\/\/api\.external\.com\//, '/public-api/']
* ```
*/
skipPatterns?: (RegExp | string)[];
}
/**
* @fileoverview CSRF protection middleware - "pit of success" API.
*/
/**
* "Pit of success" API for adding CSRF protection to a FetchClient.
* Uses smart defaults that work with most web frameworks out of the box.
*
* Default behavior:
* - Reads CSRF token from XSRF-TOKEN cookie
* - Adds X-XSRF-TOKEN header to POST, PUT, PATCH, DELETE requests
* - Skips GET/HEAD requests (they don't need CSRF protection)
*
* @param client - The FetchClient to add CSRF protection to
* @param options - Optional CSRF configuration
* @returns A new FetchClient with CSRF protection
*
* @example Basic usage (automatic cookie-based CSRF):
* ```typescript
* const client = new FetchClient();
* const protectedClient = useCSRF(client);
*
* // CSRF token automatically added to POST requests
* await protectedClient.post('/api/users', { name: 'John' });
* ```
*
* @example Custom token provider:
* ```typescript
* const protectedClient = useCSRF(client, {
* tokenProvider: () => localStorage.getItem('csrf-token') || ''
* });
* ```
*
* @example Custom header and cookie names:
* ```typescript
* const protectedClient = useCSRF(client, {
* headerName: 'X-CSRF-Token',
* cookieName: 'csrf-token'
* });
* ```
*
* @example Skip patterns for external APIs:
* ```typescript
* const protectedClient = useCSRF(client, {
* skipPatterns: [
* /^https:\/\/api\.external\.com\//, // Skip external API
* '/webhook/', // Skip webhook endpoints
* '/public-api/' // Skip public API endpoints
* ]
* });
* ```
*/
declare function useCSRF(client: FetchClient, options?: CSRFOptions): FetchClient;
/**
* @fileoverview Logging middleware types and configuration.
*/
/**
* Log levels for filtering log output.
*/
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
/**
* Log entry structure.
*/
interface LogEntry {
level: LogLevel;
timestamp: number;
method: string;
url: string;
status?: number;
duration?: number;
error?: Error;
requestHeaders?: Record<string, string>;
responseHeaders?: Record<string, string>;
requestBody?: unknown;
responseBody?: unknown;
}
/**
* Custom logger interface.
*/
interface Logger {
debug(message: string, data?: unknown): void;
info(message: string, data?: unknown): void;
warn(message: string, data?: unknown): void;
error(message: string, data?: unknown): void;
}
/**
* Logging configuration options - optimized for "pit of success".
*
* Smart defaults:
* - Logs to console
* - Info level by default
* - Excludes request/response bodies by default
* - Includes timing information
*/
interface LoggingOptions {
/**
* Minimum log level to output (default: 'info')
* Logs at this level and above will be output
*/
level?: LogLevel;
/**
* Custom logger implementation (default: console)
* Can be replaced with winston, pino, etc.
*/
logger?: Logger;
/**
* Include request headers in logs (default: false)
* May contain sensitive information
*/
includeRequestHeaders?: boolean;
/**
* Include response headers in logs (default: false)
* May contain sensitive information
*/
includeResponseHeaders?: boolean;
/**
* Include request body in logs (default: false)
* May contain sensitive information and increase log size
*/
includeRequestBody?: boolean;
/**
* Include response body in logs (default: false)
* May contain sensitive information and increase log size
*/
includeResponseBody?: boolean;
/**
* Skip logging for requests matching these URL patterns
* Useful for health checks, metrics endpoints, etc.
*
* @example
* ```typescript
* skipPatterns: ['/health', '/metrics', /\/static\//]
* ```
*/
skipPatterns?: (RegExp | string)[];
/**
* Custom log formatter function
* Allows complete customization of log output
*/
formatter?: (entry: LogEntry) => string;
}
/**
* @fileoverview Logging middleware implementation.
*/
/**
* Creates logging middleware with smart defaults.
* Logs HTTP requests and responses for debugging and monitoring.
*
* @param options - Logging configuration options
* @returns Logging middleware for use with FetchClient
*
* @example Basic logging:
* ```typescript
* const loggedClient = useLogging(client);
* // Logs all requests to console
* ```
*
* @example Custom logger:
* ```typescript
* const loggedClient = useLogging(client, {
* logger: winston.createLogger({...}),
* level: 'debug',
* includeRequestHeaders: true
* });
* ```
*/
declare function createLoggingMiddleware(options?: LoggingOptions): FetchMiddleware;
/**
* @fileoverview Logging middleware - "pit of success" API.
*/
/**
* "Pit of success" API for adding logging to a FetchClient.
* Logs HTTP requests and responses for debugging and monitoring.
*
* @param client - The FetchClient to add logging to
* @param options - Logging configuration options
* @returns A new FetchClient with logging middleware
*
* @example Basic logging to console:
* ```typescript
* const loggedClient = useLogging(client);
*
* // Logs: → GET /api/users
* // Logs: ← GET /api/users → 200 (245ms)
* await loggedClient.get('/api/users');
* ```
*
* @example Custom log level and headers:
* ```typescript
* const loggedClient = useLogging(client, {
* level: 'debug',
* includeRequestHeaders: true,
* includeResponseHeaders: true
* });
* ```
*
* @example Skip health check endpoints:
* ```typescript
* const loggedClient = useLogging(client, {
* skipPatterns: ['/health', '/metrics', '/ping']
* });
* ```
*/
declare function useLogging(client: FetchClient, options?: LoggingOptions): FetchClient;
/**
* @fileoverview Rate limiting middleware types and configuration.
*/
/**
* Rate limiting algorithm types.
*/
type RateLimitAlgorithm = 'token-bucket' | 'sliding-window' | 'fixed-window';
/**
* Rate limiting configuration options - optimized for "pit of success".
*
* Smart defaults:
* - 60 requests per minute
* - Token bucket algorithm
* - Per-client limiting
* - Graceful handling when rate limit exceeded
*/
interface RateLimitOptions {
/**
* Maximum number of requests allowed (default: 60)
*/
maxRequests?: number;
/**
* Time window in milliseconds (default: 60000 = 1 minute)
*/
windowMs?: number;
/**
* Rate limiting algorithm (default: 'token-bucket')
* - 'token-bucket': Allows bursts up to maxRequests, refills over time
* - 'sliding-window': Smooth rate limiting over rolling window
* - 'fixed-window': Fixed number of requests per fixed time window
*/
algorithm?: RateLimitAlgorithm;
/**
* Custom key generator for rate limiting scope
* Default: single global rate limit for all requests
*
* @example Per-endpoint rate limiting:
* ```typescript
* keyGenerator: (request) => request.url || 'default'
* ```
*
* @example Per-user rate limiting:
* ```typescript
* keyGenerator: (request) => {
* const auth = request.headers?.get('Authorization');
* return auth ? `user:${auth}` : 'anonymous';
* }
* ```
*/
keyGenerator?: (request: RequestInit & {
url?: string;
}) => string;
/**
* Skip rate limiting for requests matching these URL patterns
*
* @example
* ```typescript
* skipPatterns: ['/health', /^\/public\//]
* ```
*/
skipPatterns?: (RegExp | string)[];
/**
* Custom handler called when rate limit is exceeded
* Can return a custom response or void to use default behavior
*
* @param retryAfter - Milliseconds until next request is allowed
* @param request - The rate-limited request
* @returns Custom response or void for default behavior
*/
onRateLimitExceeded?: (retryAfter: number, request: RequestInit & {
url?: string;
}) => void | Promise<void> | {
data: unknown;
status: number;
statusText: string;
headers: Headers;
url: string;
ok: boolean;
error?: {
message: string;
body?: unknown;
};
} | Promise<{
data: unknown;
status: number;
statusText: string;
headers: Headers;
url: string;
ok: boolean;
error?: {
message: string;
body?: unknown;
};
}>;
}
/**
* @fileoverview Rate limiting middleware implementation.
*/
/**
* Creates rate limiting middleware - mainly for API quota management.
* Note: Rate limiting is typically a server concern, but this can help with:
* - Respecting API provider limits
* - Preventing runaway requests in bulk operations
* - Cost management for pay-per-request APIs
*/
declare function createRateLimitMiddleware(options?: RateLimitOptions): FetchMiddleware;
/**
* @fileoverview Rate limiting middleware - specialized use cases.
*/
/**
* Rate limiting middleware - mainly for API quota management.
* Note: This is primarily useful for specific scenarios like:
* - Respecting third-party API limits
* - Bulk operations that need throttling
* - Pay-per-request API cost management
*/
declare function useRateLimit(client: FetchClient, options?: RateLimitOptions): FetchClient;
/**
* @fileoverview Retry middleware types and configuration.
*/
/**
* Retry configuration options - optimized for "pit of success".
*
* Smart defaults:
* - 3 retries (4 total attempts)
* - Exponential backoff starting at 1000ms
* - Only retry on network errors and 5xx status codes
*/
interface RetryOptions {
/**
* Maximum number of retry attempts (default: 3)
* Total attempts will be maxRetries + 1
*/
maxRetries?: number;
/**
* Initial delay in milliseconds (default: 1000)
* Subsequent delays use exponential backoff
*/
delay?: number;
/**
* Backoff strategy (default: 'exponential')
* - 'exponential': delay * (2 ^ attempt)
* - 'linear': delay * attempt
* - 'fixed': always use delay
*/
backoff?: 'exponential' | 'linear' | 'fixed';
/**
* Maximum delay cap in milliseconds (default: 30000 = 30s)
* Prevents exponential backoff from getting too large
*/
maxDelay?: number;
/**
* Custom function to determine if a response should be retried
* Default: retry on network errors (status 0) and server errors (5xx)
*
* @param response - The fetch response or error
* @param attempt - Current attempt number (1-based)
* @returns true if request should be retried
*/
shouldRetry?: (response: {
status: number;
ok: boolean;
}, attempt: number) => boolean;
/**
* Optional callback called before each retry attempt
* Useful for logging or analytics
*
* @param attempt - Current attempt number (1-based)
* @param delay - Delay before this retry in ms
* @param lastResponse - The failed response that triggered the retry
*/
onRetry?: (attempt: number, delay: number, lastResponse: {
status: number;
statusText: string;
}) => void;
}
/**
* @fileoverview Retry middleware implementation with enhanced architecture.
*/
/**
* Creates a retry middleware with smart defaults.
*
* 🎯 PIT OF SUCCESS: Works great with no config, customizable when needed.
*
* Features:
* - ✅ Preserves full middleware chain on retries (unlike old implementation)
* - ✅ Exponential backoff with jitter
* - ✅ Smart retry conditions (network errors + 5xx)
* - ✅ Configurable but sensible defaults
* - ✅ Type-safe configuration
*
* @param options - Retry configuration (all optional)
* @returns Middleware function
*
* @example Basic usage:
* ```typescript
* const client = new FetchClient();
* client.use(createRetryMiddleware()); // 3 retries with exponential backoff
* ```
*
* @example Custom configuration:
* ```typescript
* const client = new FetchClient();
* client.use(createRetryMiddleware({
* maxRetries: 5,
* delay: 500,
* backoff: 'linear',
* onRetry: (attempt, delay) => console.log(`Retry ${attempt} in ${delay}ms`)
* }));
* ```
*/
declare function createRetryMiddleware(options?: RetryOptions): FetchMiddleware;
declare function useRetry(client: FetchClient, options?: RetryOptions): FetchClient;
/**
* @fileoverview Complete middleware collection for FetchClient - "pit of success" APIs.
*
* This module provides a comprehensive set of middleware for common HTTP client concerns:
* - 🔐 Authentication: Bearer token injection
* - 🛡️ Authorization: 401/403 response handling
* - 💾 Cache: Response caching with TTL
* - 🔒 CSRF: Cross-site request forgery protection
* - 📝 Logging: Request/response logging
* - 🚦 Rate Limiting: Request rate limiting with token bucket
* - 🔄 Retry: Automatic retry with backoff
*
* Each middleware follows the "pit of success" pattern with:
* - Smart defaults for common scenarios
* - Simple `use{Middleware}()` convenience functions
* - Advanced `create{Middleware}Middleware()` for custom scenarios
* - Comprehensive TypeScript support
*
* @example Quick setup with multiple middleware:
* ```typescript
* import { FetchClient } from '@fgrzl/fetch';
* import { useAuthentication, useRetry, useLogging } from '@fgrzl/fetch/middleware';
*
* const client = new FetchClient();
* const enhancedClient = useAuthentication(client, {
* tokenProvider: () => localStorage.getItem('auth-token') || ''
* })
* .pipe(useRetry, { retries: 3 })
* .pipe(useLogging);
* ```
*/
/**
* Production-ready middleware stack with authentication, retry, logging, and caching.
* Perfect for API clients that need reliability and observability.
*
* @param client - The FetchClient to enhance
* @param config - Configuration for each middleware
* @returns Enhanced FetchClient with production middleware stack
*
* @example
* ```typescript
* const apiClient = useProductionStack(new FetchClient(), {
* auth: { tokenProvider: () => getAuthToken() },
* cache: { ttl: 5 * 60 * 1000 }, // 5 minutes
* logging: { level: 'info' }
* });
* ```
*/
declare function useProductionStack(client: FetchClient, config?: {
auth?: Parameters<typeof useAuthentication>[1];
retry?: Parameters<typeof useRetry>[1];
cache?: Parameters<typeof useCache>[1];
logging?: Parameters<typeof useLogging>[1];
rateLimit?: Parameters<typeof useRateLimit>[1];
}): FetchClient;
/**
* Development-friendly middleware stack with comprehensive logging and retries.
* Perfect for local development and debugging.
*
* @param client - The FetchClient to enhance
* @param config - Configuration for development middleware
* @returns Enhanced FetchClient with development middleware stack
*
* @example
* ```typescript
* const devClient = useDevelopmentStack(new FetchClient(), {
* auth: { tokenProvider: () => 'dev-token' }
* });
* ```
*/
declare function useDevelopmentStack(client: FetchClient, config?: {
auth?: Parameters<typeof useAuthentication>[1];
}): FetchClient;
/**
* Basic middleware stack with just authentication and retry.
* Perfect for simple API clients that need minimal overhead.
*
* @param client - The FetchClient to enhance
* @param config - Basic configuration
* @returns Enhanced FetchClient with basic middleware stack
*
* @example
* ```typescript
* const basicClient = useBasicStack(new FetchClient(), {
* auth: { tokenProvider: () => getToken() }
* });
* ```
*/
declare function useBasicStack(client: FetchClient, config: {
auth: Parameters<typeof useAuthentication>[1];
}): FetchClient;
/**
* @fileoverview Main library entry point with "pit of success" architecture.
*
* This module exports everything users need in order of discoverability:
* 1. Pre-configured client with smart defaults (80% of users start here)
* 2. FetchClient for custom configurations
* 3. Individual middleware functions for specific needs
* 4. Pre-built middleware stacks for common scenarios
* 5. Types for TypeScript users
*/
/**
* 🎯 PIT OF SUCCESS: Pre-configured fetch client (Level 1 - 80% of users)
*
* This client is ready to use out of the box with production-ready middleware:
* - Authentication support (configure your token provider)
* - Automatic retries with exponential backoff
* - Response caching for GET requests
* - Request/response logging
* - Rate limiting protection
* - Same-origin credentials for session-based auth (cookies)
*
* @example Just import and use:
* ```typescript
* import api from '@fgrzl/fetch';
*
* // Works immediately - no setup required!
* const users = await api.get('/api/users');
* const newUser = await api.post('/api/users', { name: 'John' });
*
* // With query parameters
* const activeUsers = await api.get('/api/users', { status: 'active', limit: 10 });
* ```
*
* @example Set base URL for API-specific clients:
* ```typescript
* import api from '@fgrzl/fetch';
*
* // Configure base URL dynamically
* api.setBaseUrl('https://api.example.com');
*
* // Now all relative URLs are prefixed automatically
* const users = await api.get('/users'); // → GET https://api.example.com/users
* const posts = await api.get('/posts'); // → GET https://api.example.com/posts
* ```
*
* @example Configure authentication:
* ```typescript
* import api from '@fgrzl/fetch';
* import { useAuthentication } from '@fgrzl/fetch/middleware';
*
* const authClient = useAuthentication(api, {
* tokenProvider: () => localStorage.getItem('auth-token') || ''
* });
* ```
*
* @example For token-only auth (no cookies):
* ```typescript
* import { FetchClient, useAuthentication } from '@fgrzl/fetch';
*
* const tokenClient = useAuthentication(new FetchClient({
* credentials: 'omit' // Don't send cookies
* }), {
* tokenProvider: () => getJWTToken()
* });
* ```
*
* @example Production-ready API client with base URL:
* ```typescript
* import { FetchClient, useProductionStack } from '@fgrzl/fetch';
*
* // One-liner production setup with base URL
* const apiClient = useProductionStack(new FetchClient(), {
* auth: { tokenProvider: () => getAuthToken() },
* retry: { maxRetries: 3 },
* logging: { level: 'info' }
* }).setBaseUrl(process.env.API_BASE_URL || 'https://api.example.com');
*
* // Ready to use with full production features
* const users = await apiClient.get('/users');
* ```
*/
declare const api: FetchClient;
export { type AuthTokenProvider, type AuthenticationOptions, type AuthorizationOptions, type CacheEntry, type CacheKeyGenerator, type CacheOptions, type CacheStorage, FetchClient, type FetchClientOptions, FetchError, type FetchResponse, HttpError, type FetchMiddleware as InterceptMiddleware, type LogLevel, type Logger, type LoggingOptions, NetworkError, type QueryParams, type QueryValue, type RateLimitAlgorithm, type RateLimitOptions, type RetryOptions, type UnauthorizedHandler, appendQueryParams, buildQueryParams, createAuthenticationMiddleware, createAuthorizationMiddleware, createCacheMiddleware, createLoggingMiddleware, createRateLimitMiddleware, createRetryMiddleware, api as default, useAuthentication, useAuthorization, useBasicStack, useCSRF, useCache, useDevelopmentStack, useLogging, useProductionStack, useRateLimit, useRetry };