Type-safe API client with schema validation using Standard Schema
npm install what-the-fetchType-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
``bashUsing npm
npm install what-the-fetch
Usage
$3
`typescript
import { 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 }>
`$3
`typescript
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: 'john@example.com',
},
});
`$3
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:`typescript
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: 'john@example.com' },
});
// Explicit methods for clarity
await apiFetch('@put/users/:id', {
params: { id: 123 },
body: { name: 'Jane Doe', email: 'jane@example.com' },
});
await apiFetch('@delete/users/:id', { params: { id: 123 } });
$3
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 } });
`$3
You can also provide per-request headers that will be merged with shared headers:
`typescript
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',
},
}
);
`API
$3
Creates a type-safe fetch function for your API.
Parameters:
-
schema: An object mapping API paths to their schema definitions
- baseUrl: The base URL for all API requests
- sharedInit (optional): Shared RequestInit options that will be merged with per-request optionsReturns: 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 schema
- init (optional): Per-request RequestInit to customize the fetch request (merged with sharedInit)$3
Each path in your schema can have:
-
params: Schema for URL path parameters (e.g., :id) - Required for parameterized paths
- query: Schema for query string parameters
- body: Schema for request body (automatically sets method to POST)
- response: Schema for response validationAll 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.Why what-the-fetch?
Building API clients manually is error-prone and lacks type safety:
`typescript
// ❌ 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!
``typescript
// ✅ 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