Type-safe API client with schema validation using Standard Schema.
what-the-fetch is a type-safe API client library that integrates schema validation with fetch requests, leveraging the Standard Schema specification for maximum flexibility and type safety.
- Type-safe: Full TypeScript support with end-to-end type inference
- Schema validation: Built-in support for Standard Schema (compatible with Zod, Valibot, ArkType, and more)
- Flexible: Works with any schema library that implements Standard Schema
- Minimal: Small bundle size with minimal dependencies
- URL building: Integrated with fast-url for clean URL construction
# Using npm
npm install what-the-fetch
# Using bun
bun add what-the-fetch
# Using JSR (recommended for Deno)
deno add jsr:@hckhanh/what-the-fetchimport { createFetch } from 'what-the-fetch';
import { z } from 'zod';
// Define your API schema
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(),
}),
},
'/users': {
query: z.object({
limit: z.number().optional(),
offset: z.number().optional(),
}),
response: z.array(z.object({
id: z.number(),
name: z.string(),
})),
},
} as const;
// Create a typed fetch function
const apiFetch = createFetch(api, 'https://api.example.com');
// Make type-safe requests
const user = await apiFetch('/users/:id', {
params: { id: 123 },
query: { fields: 'name,email' },
});
// user is typed as { id: number; name: string; email: string }
const users = await apiFetch('/users', {
query: { limit: 10, offset: 0 },
});
// users is typed as Array<{ id: number; name: string }>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(),
}),
},
} as const;
const apiFetch = createFetch(api, 'https://api.example.com');
const newUser = await apiFetch('/users', {
body: {
name: 'John Doe',
email: '[email protected]',
},
});what-the-fetch automatically infers HTTP methods: requests with a body use POST, and requests without a body use GET. You can also explicitly specify methods using the @method prefix for clarity or when you need other HTTP methods:
const api = {
// Automatic method inference (these are equivalent)
'/users/:id': { // Uses GET (no body)
params: z.object({ id: z.number() }),
response: z.object({ id: z.number(), name: z.string() }),
},
'@get/users/:id': { // Explicitly GET - same as above
params: z.object({ id: z.number() }),
response: z.object({ id: z.number(), name: z.string() }),
},
// POST is inferred when body is present
'/users': { // Uses POST (has body)
body: z.object({ name: z.string(), email: z.string().email() }),
response: z.object({ id: z.number(), name: z.string() }),
},
// Explicit methods for PUT, PATCH, DELETE
'@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() }),
},
'@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');
// These 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 Doe', email: '[email protected]' },
});
// Explicit methods for clarity
await apiFetch('@put/users/:id', {
params: { id: 123 },
body: { name: 'Jane Doe', email: '[email protected]' },
});
await apiFetch('@delete/users/:id', { params: { id: 123 } });
### With Shared Headers
You can provide shared headers when creating the fetch function:
```typescript
const apiFetch = createFetch(
api,
'https://api.example.com',
{
headers: {
'Authorization': 'Bearer token',
},
}
);
// All requests will include the Authorization header
const user = await apiFetch('/users/:id', { params: { id: 123 } });You can also provide per-request headers that will be merged with shared headers:
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',
},
}
);Creates a type-safe fetch function for your API.
Parameters:
schema: 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)
Each path in your schema can have:
params: Schema for URL path parameters (e.g.,:id) - Required for parameterized pathsquery: Schema for query string parametersbody: Schema for request body (automatically sets method to POST)response: Schema for response validation
All schemas must implement the Standard Schema specification.
Note: If your path contains parameters (e.g., /users/:id), you must define a params schema. The library will throw an error at runtime if you attempt to use a parameterized path without a params schema.
Building API clients manually is error-prone and lacks type safety:
// ❌ No type safety, manual validation
const response = await fetch(`${baseUrl}/users/${id}?fields=${fields}`);
const data = await response.json();
// What type is data? Who knows!// ✅ Type-safe with validation
const user = await apiFetch('/users/:id', {
params: { id },
query: { fields },
});
// user is fully typed and validated!what-the-fetch handles:
- Type-safe URL construction with path and query parameters
- Automatic request/response validation
- Clean separation of concerns
- Full TypeScript inference
what-the-fetch works with any schema library that implements Standard Schema:
- Zod
- Valibot
- ArkType
- TypeSchema
- And more!
Contributions are welcome! Please feel free to submit a Pull Request.
MIT