zodsei
Version:
Contract-first type-safe HTTP client with Zod validation
533 lines (517 loc) • 16.9 kB
TypeScript
import { z } from 'zod';
export { z } from 'zod';
import { CreateAxiosDefaults } from 'axios';
import { Options } from 'ky';
/**
* HTTP adapter interface
*/
interface HttpAdapter {
/**
* Execute HTTP request
*/
request(context: RequestContext): Promise<ResponseContext>;
/**
* Adapter name
*/
readonly name: string;
}
/**
* Supported adapter types
*/
type AdapterType = 'fetch' | 'axios' | 'ky';
/**
* Adapter factory function
*/
declare function createAdapter(type: AdapterType, config?: Record<string, unknown>): Promise<HttpAdapter>;
/**
* Check if adapter is available
*/
declare function isAdapterAvailable(type: AdapterType): Promise<boolean>;
/**
* Get default adapter
*/
declare function getDefaultAdapter(config?: Record<string, any>): Promise<HttpAdapter>;
/**
* Fetch adapter configuration
*/
interface FetchAdapterConfig extends RequestInit {
timeout?: number;
}
/**
* Fetch HTTP adapter
*/
declare class FetchAdapter implements HttpAdapter {
readonly name = "fetch";
private config;
constructor(config?: FetchAdapterConfig);
request(context: RequestContext): Promise<ResponseContext>;
private createRequestInit;
private parseResponseData;
}
/**
* Axios adapter configuration
*/
type AxiosAdapterConfig = CreateAxiosDefaults;
/**
* Axios HTTP adapter
*/
declare class AxiosAdapter implements HttpAdapter {
readonly name = "axios";
private config;
private axios;
constructor(config?: AxiosAdapterConfig);
private getAxios;
request(context: RequestContext): Promise<ResponseContext>;
private createAxiosConfig;
}
/**
* Ky adapter configuration
*/
type KyAdapterConfig = Options;
/**
* Ky HTTP adapter
*/
declare class KyAdapter implements HttpAdapter {
readonly name = "ky";
private config;
private ky;
constructor(config?: KyAdapterConfig);
private getKy;
request(context: RequestContext): Promise<ResponseContext>;
private createKyOptions;
private parseResponseData;
}
/**
* Schema inference and extraction utilities
*/
/**
* Extract request type from endpoint definition
*/
type InferRequestType$1<T extends EndpointDefinition> = T['request'] extends z.ZodSchema ? z.infer<T['request']> : void;
/**
* Extract response type from endpoint definition
*/
type InferResponseType$1<T extends EndpointDefinition> = T['response'] extends z.ZodSchema ? z.infer<T['response']> : unknown;
/**
* Extract all endpoint types from a contract
*/
type InferContractTypes<T extends Contract> = {
[K in keyof T]: T[K] extends EndpointDefinition ? {
request: InferRequestType$1<T[K]>;
response: InferResponseType$1<T[K]>;
endpoint: T[K];
} : T[K] extends Contract ? InferContractTypes<T[K]> : never;
};
/**
* Schema extraction utilities
*/
declare class SchemaExtractor<T extends Contract> {
private contract;
constructor(contract: T);
/**
* Get endpoint definition by path
*/
getEndpoint<K extends keyof T>(path: K): T[K] extends EndpointDefinition ? T[K] : never;
/**
* Get nested contract by path
*/
getNested<K extends keyof T>(path: K): T[K] extends Contract ? SchemaExtractor<T[K]> : never;
/**
* Get request schema for an endpoint
*/
getRequestSchema<K extends keyof T>(path: K): T[K] extends EndpointDefinition ? T[K]['request'] : never;
/**
* Get response schema for an endpoint
*/
getResponseSchema<K extends keyof T>(path: K): T[K] extends EndpointDefinition ? T[K]['response'] : never;
/**
* Get all schemas for an endpoint
*/
getEndpointSchemas<K extends keyof T>(path: K): T[K] extends EndpointDefinition ? {
request: T[K]['request'];
response: T[K]['response'];
endpoint: T[K];
} : never;
/**
* Get all endpoint paths in the contract
*/
getEndpointPaths(): Array<keyof T>;
/**
* Get all nested contract paths
*/
getNestedPaths(): Array<keyof T>;
/**
* Generate OpenAPI-like schema description
*/
describeEndpoint<K extends keyof T>(path: K): T[K] extends EndpointDefinition ? {
path: string;
method: string;
requestSchema: z.ZodSchema | undefined;
responseSchema: z.ZodSchema | undefined;
requestType: string;
responseType: string;
} : never;
/**
* Generate schema description for documentation
*/
private getSchemaDescription;
/**
* Get basic Zod type description
*/
private getZodTypeDescription;
/**
* Check if a value is an endpoint definition
*/
private isEndpointDefinition;
/**
* Check if a value is a nested contract
*/
private isNestedContract;
}
/**
* Create a schema extractor for a contract
*/
declare function createSchemaExtractor<T extends Contract>(contract: T): SchemaExtractor<T>;
/**
* Utility type to infer endpoint method signature
*/
type InferEndpointMethod<T extends EndpointDefinition> = (...args: T['request'] extends z.ZodSchema ? [data: InferRequestType$1<T>] : []) => Promise<InferResponseType$1<T>>;
/**
* Utility to extract type information at runtime
*/
declare function extractTypeInfo<T extends EndpointDefinition>(endpoint: T): {
requestSchema: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>> | undefined;
responseSchema: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>> | undefined;
method: HttpMethod;
path: string;
hasRequestSchema: boolean;
hasResponseSchema: boolean;
};
type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head' | 'options';
interface EndpointDefinition {
path: string;
method: HttpMethod;
request?: z.ZodSchema;
response?: z.ZodSchema;
}
/**
* Contract definition - can be nested
*/
interface Contract {
[key: string]: EndpointDefinition | Contract;
}
/**
* Helper function to define a contract with proper type inference
* Preserves literal types while ensuring type safety
* Supports nested contracts
*/
declare function defineContract<T extends Contract>(contract: T): T;
/**
* Create client type from contract - supports nested access with schema support
*/
type ApiClient<T extends Contract> = {
[K in keyof T]: T[K] extends EndpointDefinition ? EndpointMethodWithSchema<T[K]> : T[K] extends Contract ? ApiClient<T[K]> : never;
} & {
$schema: SchemaExtractor<T>;
};
interface BaseClientConfig {
baseUrl: string;
validateRequest?: boolean;
validateResponse?: boolean;
headers?: Record<string, string>;
timeout?: number;
retries?: number;
middleware?: Middleware[];
}
type ClientConfig = (BaseClientConfig & {
adapter?: 'fetch';
adapterConfig?: FetchAdapterConfig;
}) | (BaseClientConfig & {
adapter: 'axios';
adapterConfig?: AxiosAdapterConfig;
}) | (BaseClientConfig & {
adapter: 'ky';
adapterConfig?: KyAdapterConfig;
}) | (BaseClientConfig & {
adapter?: undefined;
adapterConfig?: FetchAdapterConfig;
});
interface InternalClientConfig {
baseUrl: string;
validateRequest: boolean;
validateResponse: boolean;
headers: Record<string, string>;
timeout: number;
retries: number;
middleware: Middleware[];
adapter: AdapterType | undefined;
adapterConfig: Record<string, any>;
}
type Middleware = (request: RequestContext, next: (request: RequestContext) => Promise<ResponseContext>) => Promise<ResponseContext>;
interface RequestContext {
url: string;
method: HttpMethod;
headers: Record<string, string>;
body?: any;
params?: Record<string, string>;
query?: Record<string, any>;
}
interface ResponseContext {
status: number;
statusText: string;
headers: Record<string, string>;
data: any;
}
type ExtractPathParams<T extends string> = T extends `${infer _Start}:${infer Param}/${infer Rest}` ? {
[K in Param]: string;
} & ExtractPathParams<`/${Rest}`> : T extends `${infer _Start}:${infer Param}` ? {
[K in Param]: string;
} : object;
type SeparateRequestData<T> = T extends Record<string, any> ? {
pathParams: ExtractPathParams<string>;
queryParams: Omit<T, keyof ExtractPathParams<string>>;
body: T;
} : {
pathParams: object;
queryParams: object;
body: T;
};
type InferRequestType<T extends EndpointDefinition> = T['request'] extends z.ZodSchema ? z.infer<T['request']> : void;
type InferResponseType<T extends EndpointDefinition> = T['response'] extends z.ZodSchema ? z.infer<T['response']> : unknown;
interface EndpointMethodWithSchema<T extends EndpointDefinition> {
(...args: T['request'] extends z.ZodSchema ? [data: InferRequestType<T>] : []): Promise<InferResponseType<T>>;
schema: {
request: T['request'];
response: T['response'];
endpoint: T;
};
infer: {
request: InferRequestType<T>;
response: InferResponseType<T>;
};
}
type EnhancedApiClient<T extends Contract> = ApiClient<T>;
/**
* Zodsei client core implementation
*/
declare class ZodseiClient<T extends Contract> {
private readonly contract;
private readonly config;
private readonly middlewareExecutor;
private adapter;
readonly $schema: SchemaExtractor<T>;
constructor(contract: T, config: ClientConfig);
/**
* Normalize configuration
*/
private normalizeConfig;
/**
* Check if a value is an endpoint definition
*/
private isEndpointDefinition;
/**
* Check if a value is a nested contract
*/
private isNestedContract;
/**
* Create nested client for sub-contracts
*/
private createNestedClient;
/**
* Create endpoint method with schema access
*/
private createEndpointMethod;
/**
* Execute endpoint request
*/
private executeEndpoint;
/**
* Build request context
*/
private buildRequestContext;
/**
* Get adapter
*/
private getAdapter;
/**
* Execute HTTP request
*/
private executeHttpRequest;
/**
* Get configuration
*/
getConfig(): Readonly<InternalClientConfig>;
/**
* Get contract
*/
getContract(): Readonly<T>;
/**
* Add middleware
*/
use(middleware: any): void;
}
/**
* Create client with enhanced schema support
*/
declare function createClient<T extends Contract>(contract: T, config: ClientConfig): ZodseiClient<T> & ApiClient<T>;
declare class ZodseiError extends Error {
readonly code: string;
constructor(message: string, code: string);
}
declare class ValidationError extends ZodseiError {
readonly issues: z.ZodIssue[];
readonly type: 'request' | 'response';
constructor(message: string, issues: z.ZodIssue[], type?: 'request' | 'response');
static fromZodError(error: z.ZodError, type?: 'request' | 'response'): ValidationError;
}
declare class HttpError extends ZodseiError {
readonly status: number;
readonly statusText: string;
readonly response?: any | undefined;
constructor(message: string, status: number, statusText: string, response?: any | undefined);
static fromResponse(response: Response, data?: any): HttpError;
}
declare class NetworkError extends ZodseiError {
readonly originalError: Error;
constructor(message: string, originalError: Error);
}
declare class ConfigError extends ZodseiError {
constructor(message: string);
}
declare class TimeoutError extends ZodseiError {
constructor(timeout: number);
}
/**
* Validation utility functions
*/
declare function validateRequest<T>(schema: z.ZodSchema<T> | undefined, data: unknown): T;
declare function validateResponse<T>(schema: z.ZodSchema<T> | undefined, data: unknown): T;
declare function safeParseRequest<T>(schema: z.ZodSchema<T> | undefined, data: unknown): {
success: true;
data: T;
} | {
success: false;
error: ValidationError;
};
declare function safeParseResponse<T>(schema: z.ZodSchema<T> | undefined, data: unknown): {
success: true;
data: T;
} | {
success: false;
error: ValidationError;
};
declare function createValidator<T>(schema: z.ZodSchema<T> | undefined, enabled: boolean): {
validateRequest: (data: unknown) => T;
validateResponse: (data: unknown) => T;
safeParseRequest: (data: unknown) => {
success: false;
error: ValidationError;
} | {
success: true;
data: T;
};
safeParseResponse: (data: unknown) => {
success: false;
error: ValidationError;
} | {
success: true;
data: T;
};
};
/**
* Middleware system
*/
declare class MiddlewareExecutor {
private middleware;
constructor(middleware?: Middleware[]);
execute(request: RequestContext, finalHandler: (request: RequestContext) => Promise<ResponseContext>): Promise<ResponseContext>;
use(middleware: Middleware): void;
getMiddleware(): Middleware[];
}
declare function createMiddlewareExecutor(middleware?: Middleware[]): MiddlewareExecutor;
declare function composeMiddleware(...middleware: Middleware[]): Middleware;
/**
* Retry middleware configuration
*/
interface RetryConfig {
retries: number;
delay: number;
backoff?: 'linear' | 'exponential';
retryCondition?: (error: Error) => boolean;
onRetry?: (attempt: number, error: Error) => void;
}
/**
* Create retry middleware
*/
declare function retryMiddleware(config: RetryConfig): Middleware;
/**
* Create simple retry middleware
*/
declare function simpleRetry(retries: number, delay?: number): Middleware;
/**
* Cache middleware configuration
*/
interface CacheConfig {
ttl: number;
keyGenerator?: (request: RequestContext) => string;
shouldCache?: (request: RequestContext, response: ResponseContext) => boolean;
storage?: CacheStorage;
}
/**
* Cache storage interface
*/
interface CacheStorage {
get(key: string): Promise<CacheEntry | null>;
set(key: string, entry: CacheEntry): Promise<void>;
delete(key: string): Promise<void>;
clear(): Promise<void>;
}
/**
* Cache entry
*/
interface CacheEntry {
data: ResponseContext;
timestamp: number;
ttl: number;
}
/**
* Memory cache storage implementation
*/
declare class MemoryCacheStorage implements CacheStorage {
private cache;
get(key: string): Promise<CacheEntry | null>;
set(key: string, entry: CacheEntry): Promise<void>;
delete(key: string): Promise<void>;
clear(): Promise<void>;
size(): number;
cleanup(): void;
}
/**
* Create cache middleware
*/
declare function cacheMiddleware(config: CacheConfig): Middleware;
/**
* Create simple cache middleware
*/
declare function simpleCache(ttl: number): Middleware;
/**
* Path handling utility functions
*/
declare function extractPathParamNames(path: string): string[];
declare function replacePath(path: string, params: Record<string, string>): string;
declare function buildQueryString(params: Record<string, any>): string;
declare function buildUrl(baseUrl: string, path: string, query?: Record<string, any>): string;
declare function separateParams(path: string, data: Record<string, any> | null | undefined): {
pathParams: Record<string, string>;
queryParams: Record<string, any>;
};
declare function shouldHaveBody(method: string): boolean;
/**
* Request handling utility functions
*
* Note: This file contains utility functions that were originally designed
* for general HTTP request handling, but are now handled by individual adapters.
* These functions are kept for backward compatibility and potential future use.
*/
declare function mergeHeaders(defaultHeaders: Record<string, string>, requestHeaders?: Record<string, string>): Record<string, string>;
export { type AdapterType, type ApiClient, AxiosAdapter, type AxiosAdapterConfig, type CacheConfig, type CacheEntry, type CacheStorage, type ClientConfig, ConfigError, type Contract, type EndpointDefinition, type EndpointMethodWithSchema, type EnhancedApiClient, type ExtractPathParams, FetchAdapter, type FetchAdapterConfig, type HttpAdapter, HttpError, type HttpMethod, type InferContractTypes, type InferEndpointMethod, type InferRequestType$1 as InferRequestType, type InferResponseType$1 as InferResponseType, KyAdapter, type KyAdapterConfig, MemoryCacheStorage, type Middleware, NetworkError, type RequestContext, type ResponseContext, SchemaExtractor, type SeparateRequestData, TimeoutError, ValidationError, ZodseiClient, ZodseiError, buildQueryString, buildUrl, cacheMiddleware, composeMiddleware, createAdapter, createClient, createMiddlewareExecutor, createSchemaExtractor, createValidator, defineContract, extractPathParamNames, extractTypeInfo, getDefaultAdapter, isAdapterAvailable, mergeHeaders, replacePath, retryMiddleware, safeParseRequest, safeParseResponse, separateParams, shouldHaveBody, simpleCache, simpleRetry, validateRequest, validateResponse };