API Reference
Complete API documentation for what-the-fetch with detailed examples and type signatures
Main Function
createFetch()
Creates a type-safe fetch function for your API.
function createFetch<Schema extends ApiSchema>(
apis: Schema,
baseUrl: string,
sharedInit?: RequestInit
): <Path extends ApiPath<Schema>>(
path: Path,
options?: FetchOptions<Schema, Path>,
init?: RequestInit
) => Promise<ApiData<Schema, Path, 'response'>>Parameters:
apis- An object mapping API paths to their schema definitionsbaseUrl- The base URL for all API requestssharedInit(optional) - Shared RequestInit options that will be merged with per-request options
Returns: A typed fetch function that accepts:
path- The API path (must be a key from your schema)options(optional) - Request options (params, query, body) based on the path's schemainit(optional) - Per-request RequestInit to customize the fetch request (merged with sharedInit)
Example:
import { createFetch } from 'what-the-fetch';
import { z } from 'zod';
const api = {
'/users/:id': {
params: z.object({ id: z.number() }),
query: z.object({ fields: z.string().optional() }),
response: z.object({
id: z.number(),
name: z.string(),
email: z.string()
})
}
};
const apiFetch = createFetch(api, 'https://api.example.com');
const user = await apiFetch('/users/:id', {
params: { id: 123 },
query: { fields: 'name,email' }
});
// user is typed as { id: number; name: string; email: string }Type Definitions
ApiSchema
Schema definition for an API. Maps API paths to their schema definitions.
type ApiSchema = Record<
string,
{
params?: StandardSchemaV1<Record<string, unknown>>
query?: StandardSchemaV1<Record<string, unknown>>
body?: StandardSchemaV1<Record<string, unknown>>
response?: StandardSchemaV1<Record<string, unknown>>
}
>Each path in your schema can have:
params- Schema for URL path parameters (e.g.,:id)query- Schema for query string parametersbody- Schema for request body (automatically sets method to POST)response- Schema for response validation
Important: If your path contains parameters (e.g., /users/:id), you must define a params schema. Attempting to use a parameterized path without a params schema will throw an error at runtime.
Example:
import { z } from 'zod';
const api: ApiSchema = {
'/users/:id': {
params: z.object({ id: z.number() }),
query: z.object({ fields: z.string().optional() }),
response: z.object({ id: z.number(), name: z.string() })
},
'/users': {
// GET /users - List users with pagination
query: z.object({
limit: z.number().optional(),
offset: z.number().optional()
}),
response: z.array(z.object({ id: z.number(), name: z.string() }))
}
};ApiPath<T>
Extract valid API paths from an API schema.
type ApiPath<T extends ApiSchema> = keyof T & stringExample:
const api = {
'/users/:id': { /* ... */ },
'/posts': { /* ... */ }
};
type Path = ApiPath<typeof api>;
// Path = '/users/:id' | '/posts'FetchOptions<T, Path>
Extract the required fetch options for a specific API path.
type FetchOptions<T extends ApiSchema, Path extends ApiPath<T>> =
// Infers correct types for params, query, and bodyAutomatically infers the correct types for params, query, and body based on the schema definition for the given path.
Example:
const api = {
'/users/:id': {
params: z.object({ id: z.number() }),
query: z.object({ fields: z.string().optional() })
}
};
type Options = FetchOptions<typeof api, '/users/:id'>;
// Options = { params: { id: number }, query?: { fields?: string } }ApiData<Schemas, Path, Option>
Extract the inferred data type for a specific schema option from an API path.
type ApiData<
Schemas extends ApiSchema,
Path extends ApiPath<Schemas>,
Option extends keyof Schemas[Path]
>This is a flexible type that extracts the inferred TypeScript type for any schema option ('params', 'query', 'body', or 'response') from a specific API path. This replaces the older ApiResponse type and provides more flexibility for working with different parts of your API schema.
Parameters:
Schemas- The complete API schema typePath- The specific API path to extract fromOption- The schema option to extract ('params','query','body', or'response')
Examples:
const api = {
'/users/:id': {
params: z.object({ id: z.number() }),
query: z.object({ fields: z.string().optional() }),
response: z.object({ id: z.number(), name: z.string() })
}
};
// Extract params type
type UserParams = ApiData<typeof api, '/users/:id', 'params'>;
// UserParams = { id: number }
// Extract query type
type UserQuery = ApiData<typeof api, '/users/:id', 'query'>;
// UserQuery = { fields?: string }
// Extract response type
type UserResponse = ApiData<typeof api, '/users/:id', 'response'>;
// UserResponse = { id: number; name: string }Note: For backward compatibility, you can create a type alias:
type ApiResponse<T extends ApiSchema, Path extends ApiPath<T>> =
ApiData<T, Path, 'response'>;Request Validation
what-the-fetch automatically validates all request data (params, query, and body) as well as response data using the schemas you provide. The validation happens concurrently for optimal performance.
Validation Behavior
- Params validation: If your path contains parameters (e.g.,
/users/:id), you must provide aparamsschema. The library will throw an error if a parameterized path is missing a params schema. - Query validation: Query parameters are validated if a
queryschema is provided. - Body validation: Request body is validated if a
bodyschema is provided. - Response validation: Response data is validated if a
responseschema is provided. - Concurrent validation: All request validations (params, query, body) run concurrently using
Promise.all()for better performance.
Validation Errors
When validation fails, the library throws an error with details about the validation issues:
const api = {
'/users/:id': {
params: z.object({ id: z.number() }),
response: z.object({ id: z.number(), name: z.string() })
}
};
const apiFetch = createFetch(api, 'https://api.example.com');
// This will throw a validation error
try {
await apiFetch('/users/:id', { params: { id: 'invalid' } });
} catch (error) {
console.error(error.message); // "Validation failed: [...]"
}Required Params Schema
If your path contains URL parameters, you must define a params schema:
// ❌ This will throw an error
const badApi = {
'/users/:id': {
response: z.object({ id: z.number(), name: z.string() })
// Missing params schema!
}
};
// ✅ This is correct
const goodApi = {
'/users/:id': {
params: z.object({ id: z.number() }),
response: z.object({ id: z.number(), name: z.string() })
}
};The library detects parameterized paths using the pattern /:paramName and will throw this error if no params schema is found:
Path contains parameters but no "params" schema is defined.Request Examples
GET Request with Path Parameters
const api = {
'/users/:id': {
params: z.object({ id: z.number() }),
response: z.object({
id: z.number(),
name: z.string(),
email: z.string()
})
}
};
const apiFetch = createFetch(api, 'https://api.example.com');
const user = await apiFetch('/users/:id', {
params: { id: 123 }
});
// GET https://api.example.com/users/123GET Request with Query Parameters
const api = {
'/users': {
query: z.object({
limit: z.number().optional(),
offset: z.number().optional(),
sort: z.string().optional()
}),
response: z.array(z.object({
id: z.number(),
name: z.string()
}))
}
};
const apiFetch = createFetch(api, 'https://api.example.com');
const users = await apiFetch('/users', {
query: { limit: 10, offset: 0, sort: 'name' }
});
// GET https://api.example.com/users?limit=10&offset=0&sort=nameGET Request with Both Path and Query Parameters
const api = {
'/users/:id/posts': {
params: z.object({ id: z.number() }),
query: z.object({
limit: z.number().optional(),
published: z.boolean().optional()
}),
response: z.array(z.object({
id: z.number(),
title: z.string()
}))
}
};
const apiFetch = createFetch(api, 'https://api.example.com');
const posts = await apiFetch('/users/:id/posts', {
params: { id: 123 },
query: { limit: 10, published: true }
});
// GET https://api.example.com/users/123/posts?limit=10&published=truePOST Request with Body
const api = {
'/users': {
body: z.object({
name: z.string(),
email: z.string().email()
}),
response: z.object({
id: z.number(),
name: z.string(),
email: z.string()
})
}
};
const apiFetch = createFetch(api, 'https://api.example.com');
const newUser = await apiFetch('/users', {
body: {
name: 'John Doe',
email: 'john@example.com'
}
});
// POST https://api.example.com/users
// Content-Type: application/json
// Body: {"name":"John Doe","email":"john@example.com"}Custom Headers and Options
const api = {
'/users/:id': {
params: z.object({ id: z.number() }),
response: z.object({ id: z.number(), name: z.string() })
}
};
const apiFetch = createFetch(api, 'https://api.example.com');
const user = await apiFetch(
'/users/:id',
{ params: { id: 123 } },
{
headers: {
'Authorization': 'Bearer token',
'X-Custom-Header': 'value'
},
cache: 'no-cache',
signal: abortController.signal
}
);HTTP Methods
what-the-fetch automatically infers HTTP methods based on the presence of a request body. You can optionally use the @method prefix for explicit control or to specify methods other than GET and POST.
Automatic Method Inference
By default, what-the-fetch infers the HTTP method:
- Requests with a body →
POST - Requests without a body →
GET
const api = {
'/users/:id': { /* Uses GET (no body) */ },
'/users': { body: z.object({...}), /* Uses POST (has body) */ }
};Method Prefix Syntax (Optional)
For explicit control or to use other HTTP methods, add the @method prefix:
const api = {
'@get/resource': { /* Explicit GET - same as /resource */ },
'/resource': { /* Implicit GET */ },
'@post/resource': { /* Explicit POST */ },
'@put/resource/:id': { /* PUT - prefix required */ },
'@patch/resource/:id': { /* PATCH - prefix required */ },
'@delete/resource/:id': { /* DELETE - prefix required */ }
};Note: /users/:id and @get/users/:id are completely equivalent and both result in a GET request.
Supported HTTP Methods
All standard HTTP methods are supported:
@get- GET requests (retrieve data)@post- POST requests (create new resources)@put- PUT requests (replace existing resources)@patch- PATCH requests (partially update resources)@delete- DELETE requests (remove resources)@head- HEAD requests (retrieve headers only)@options- OPTIONS requests (check available methods)
Complete Example
import { createFetch } from 'what-the-fetch';
import { z } from 'zod';
const api = {
// GET - Retrieve a user
'@get/users/:id': {
params: z.object({ id: z.number() }),
response: z.object({
id: z.number(),
name: z.string(),
email: z.string()
})
},
// POST - Create a new user
'@post/users': {
body: z.object({
name: z.string(),
email: z.string().email()
}),
response: z.object({
id: z.number(),
name: z.string(),
email: z.string()
})
},
// PUT - Replace entire user
'@put/users/:id': {
params: z.object({ id: z.number() }),
body: z.object({
name: z.string(),
email: z.string().email()
}),
response: z.object({
id: z.number(),
name: z.string(),
email: z.string()
})
},
// PATCH - Partially update user
'@patch/users/:id': {
params: z.object({ id: z.number() }),
body: z.object({
name: z.string().optional(),
email: z.string().email().optional()
}),
response: z.object({
id: z.number(),
name: z.string(),
email: z.string()
})
},
// DELETE - Remove a user
'@delete/users/:id': {
params: z.object({ id: z.number() }),
response: z.object({ success: z.boolean() })
}
} as const;
const apiFetch = createFetch(api, 'https://api.example.com');
// Make requests with explicit methods
const user = await apiFetch('@get/users/:id', { params: { id: 123 } });
// GET https://api.example.com/users/123
const newUser = await apiFetch('@post/users', {
body: { name: 'John Doe', email: 'john@example.com' }
});
// POST https://api.example.com/users
await apiFetch('@put/users/:id', {
params: { id: 123 },
body: { name: 'Jane Doe', email: 'jane@example.com' }
});
// PUT https://api.example.com/users/123
await apiFetch('@patch/users/:id', {
params: { id: 123 },
body: { name: 'Jane Smith' }
});
// PATCH https://api.example.com/users/123
await apiFetch('@delete/users/:id', { params: { id: 123 } });
// DELETE https://api.example.com/users/123Equivalence Examples
The following path definitions are equivalent:
const api = {
// These two are identical - both use GET
'/users/:id': {
params: z.object({ id: z.number() }),
response: z.object({ id: z.number(), name: z.string() })
},
'@get/users/:id': { // Same as above - @get is optional
params: z.object({ id: z.number() }),
response: z.object({ id: z.number(), name: z.string() })
},
// POST is inferred from body
'/users': {
body: z.object({ name: z.string(), email: z.string() }),
response: z.object({ id: z.number(), name: z.string() })
}
};
const apiFetch = createFetch(api, 'https://api.example.com');
// Both calls are equivalent - both use GET
const user1 = await apiFetch('/users/:id', { params: { id: 123 } });
const user2 = await apiFetch('@get/users/:id', { params: { id: 123 } });
// POST (inferred from body)
const newUser = await apiFetch('/users', {
body: { name: 'John', email: 'john@example.com' }
});Method inference rules:
- Request has a
body→POST - Request has no
body→GET - Method prefix specified → Uses that method
Method Prefix with Path Parameters
The method prefix works seamlessly with path parameters:
const api = {
'@put/users/:userId/posts/:postId': {
params: z.object({
userId: z.number(),
postId: z.number()
}),
body: z.object({ title: z.string(), content: z.string() }),
response: z.object({ success: z.boolean() })
}
};
const apiFetch = createFetch(api, 'https://api.example.com');
await apiFetch('@put/users/:userId/posts/:postId', {
params: { userId: 123, postId: 456 },
body: { title: 'Updated', content: 'New content' }
});
// PUT https://api.example.com/users/123/posts/456Case Insensitive
Method prefixes are case-insensitive and converted to uppercase:
const api = {
'@GET/users': { /* ... */ },
'@get/posts': { /* ... */ },
'@GeT/comments': { /* ... */ }
};
// All are treated as GETBest Practices
- Be explicit with methods: Use method prefixes for clarity, especially for PUT, PATCH, and DELETE operations
- RESTful design: Follow REST conventions:
@getfor retrieval@postfor creation@putfor full replacement@patchfor partial updates@deletefor removal
- Consistency: Choose either to always use method prefixes or rely on automatic detection - be consistent across your API schema
Advanced Usage
Shared Request Configuration
Use the third parameter of createFetch() to set shared options for all requests:
import { createFetch } from 'what-the-fetch';
import { z } from 'zod';
const api = {
'/users/:id': {
params: z.object({ id: z.number() }),
response: z.object({ id: z.number(), name: z.string() })
},
'/posts': {
response: z.array(z.object({ id: z.number(), title: z.string() }))
}
};
// Create fetch with shared headers that apply to all requests
const apiFetch = createFetch(
api,
'https://api.example.com',
{
headers: {
'Authorization': 'Bearer my-token',
'X-API-Version': '2.0'
},
cache: 'no-cache'
}
);
// All requests automatically include shared headers
const user = await apiFetch('/users/:id', { params: { id: 123 } });
const posts = await apiFetch('/posts');
// Override or add additional headers per request
const userWithCustomHeader = await apiFetch(
'/users/:id',
{ params: { id: 456 } },
{
headers: {
'X-Custom-Header': 'value' // Merged with shared headers
}
}
);Multiple Endpoints with Shared Schemas
import { createFetch } from 'what-the-fetch';
import { z } from 'zod';
// Define reusable schemas
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string()
});
const PostSchema = z.object({
id: z.number(),
title: z.string(),
content: z.string(),
authorId: z.number()
});
// Use schemas in multiple endpoints
const api = {
'/users/:id': {
params: z.object({ id: z.number() }),
response: UserSchema
},
'/users': {
response: z.array(UserSchema)
},
'/posts/:id': {
params: z.object({ id: z.number() }),
response: PostSchema
},
'/users/:id/posts': {
params: z.object({ id: z.number() }),
response: z.array(PostSchema)
}
};
const apiFetch = createFetch(api, 'https://api.example.com');Building a Complete API Client
import { createFetch } from 'what-the-fetch';
import { z } from 'zod';
// Define comprehensive API schema
const api = {
'/users': {
// GET /users - List users with pagination
query: z.object({
page: z.number().optional(),
limit: z.number().optional()
}),
response: z.object({
users: z.array(z.object({
id: z.number(),
name: z.string()
})),
total: z.number()
})
},
'/users/:id': {
// GET /users/:id - Get user by ID
params: z.object({ id: z.number() }),
response: z.object({
id: z.number(),
name: z.string(),
email: z.string(),
createdAt: z.string()
})
},
'/users/create': {
// POST /users/create - Create new user
body: z.object({
name: z.string().min(3),
email: z.string().email(),
password: z.string().min(8)
}),
response: z.object({
id: z.number(),
name: z.string(),
email: z.string()
})
},
'/users/:id/update': {
// PATCH /users/:id/update - Update user
params: z.object({ id: z.number() }),
body: z.object({
name: z.string().min(3).optional(),
email: z.string().email().optional()
}),
response: z.object({
id: z.number(),
name: z.string(),
email: z.string()
})
}
} as const;
// Create client with shared configuration
class APIClient {
private fetch;
constructor(baseUrl: string, apiKey: string) {
// Use shared headers for authentication
this.fetch = createFetch(
api,
baseUrl,
{
headers: {
'Authorization': `Bearer ${apiKey}`
}
}
);
}
async getUsers(page = 1, limit = 10) {
// No need to pass auth header - it's shared
return this.fetch('/users', { query: { page, limit } });
}
async getUser(id: number) {
return this.fetch('/users/:id', { params: { id } });
}
async createUser(data: { name: string; email: string; password: string }) {
return this.fetch('/users/create', { body: data });
}
async updateUser(id: number, data: { name?: string; email?: string }) {
return this.fetch('/users/:id/update', { params: { id }, body: data });
}
}
// Usage
const client = new APIClient('https://api.example.com', 'your-api-key');
const users = await client.getUsers(1, 20);
const user = await client.getUser(123);Standard Schema Support
what-the-fetch works with any schema library that implements Standard Schema:
Zod
import { z } from 'zod';
const api = {
'/users/:id': {
params: z.object({ id: z.number() }),
response: z.object({ id: z.number(), name: z.string() })
}
};Valibot
import * as v from 'valibot';
const api = {
'/users/:id': {
params: v.object({ id: v.number() }),
response: v.object({ id: v.number(), name: v.string() })
}
};ArkType
import { type } from 'arktype';
const api = {
'/users/:id': {
params: type({ id: 'number' }),
response: type({ id: 'number', name: 'string' })
}
};Error Handling
HTTP Errors
what-the-fetch automatically throws an error for non-2xx status codes:
try {
const user = await apiFetch('/users/:id', { params: { id: 999 } });
} catch (error) {
console.error('HTTP error:', error.message);
// → "HTTP error! status: 404"
}Validation Errors
Schema validation errors are thrown when response doesn't match the schema:
const api = {
'/users/:id': {
params: z.object({ id: z.number() }),
response: z.object({
id: z.number(),
name: z.string() // Required field
})
}
};
try {
// If server returns { id: 123 } without name field
const user = await apiFetch('/users/:id', { params: { id: 123 } });
} catch (error) {
console.error('Validation error:', error);
// Schema validation failed
}TypeScript Features
Full Type Inference
const api = {
'/users/:id': {
params: z.object({ id: z.number() }),
response: z.object({ id: z.number(), name: z.string() })
}
} as const;
const apiFetch = createFetch(api, 'https://api.example.com');
// TypeScript infers everything:
const user = await apiFetch('/users/:id', {
params: { id: 123 } // TypeScript knows params is required and id must be number
});
// TypeScript knows user is { id: number; name: string }Autocomplete Support
When using what-the-fetch in TypeScript, you get full autocomplete for:
- Available API paths
- Required options (params, query, body)
- Field names and types
- Response properties
Compile-Time Validation
// ✅ This compiles
await apiFetch('/users/:id', { params: { id: 123 } });
// ❌ TypeScript error: Property 'params' is missing
await apiFetch('/users/:id', {});
// ❌ TypeScript error: Type 'string' is not assignable to type 'number'
await apiFetch('/users/:id', { params: { id: '123' } });
// ❌ TypeScript error: '/invalid' is not assignable to type '/users/:id' | ...
await apiFetch('/invalid', {});Performance Considerations
what-the-fetch is designed to be performant:
- Small bundle size: Minimal dependencies (only fast-url for URL building and Standard Schema spec)
- Efficient URL building: Leverages fast-url library's optimized implementation
- Type-level computation: Most type checking happens at compile time
- Runtime validation: Only validates responses, not TypeScript types
Bundle Size
what-the-fetch adds minimal overhead to your bundle:
- Core library: ~2KB minified + gzipped
- Plus your choice of schema library (Zod, Valibot, etc.)
Browser and Runtime Compatibility
what-the-fetch works in all modern JavaScript environments:
- ✅ Node.js 18+
- ✅ Bun
- ✅ Deno
- ✅ Modern browsers (Chrome, Firefox, Safari, Edge)
- ✅ React Native
- ✅ Electron
Requirements:
fetchAPI support (built-in in modern environments)- Standard Schema-compatible validation library