Getting Started
Quick start guide to install and use what-the-fetch in your JavaScript or TypeScript project
Installation
Install what-the-fetch using your preferred package manager:
bun add what-the-fetchpnpm add what-the-fetchyarn add what-the-fetchnpm install what-the-fetchdeno add jsr:@hckhanh/what-the-fetchYou'll also need a schema validation library that implements Standard Schema:
npm install zodnpm install valibotnpm install arktypeTypeScript Support
what-the-fetch is written in TypeScript and includes type definitions out of the
box. No need to install separate @types packages!
Basic Usage
Importing
import { createFetch } from 'what-the-fetch';
import { z } from 'zod'; // or your preferred schema libraryQuick Examples
Here are some common use cases to get you started:
Simple GET Request
Define a schema and make a type-safe request:
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(),
email: z.string()
})
}
};
const apiFetch = createFetch(api, 'https://api.example.com');
const user = await apiFetch('/users/:id', {
params: { id: 123 }
});
// user is typed as { id: number; name: string; email: string }With Query Parameters
Add query parameters to your request:
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: 20, sort: 'name' }
});
// users is typed as Array<{ id: number; name: string }>POST Request with Body
Send data in the request 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'
}
});
// newUser is typed as { id: number; name: string; email: string }Using HTTP Methods
what-the-fetch automatically infers HTTP methods, but you can also specify them explicitly using the @method prefix:
const api = {
// Automatic 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': { // Explicit 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 other HTTP verbs
'@put/users/:id': {
params: z.object({ id: z.number() }),
body: z.object({ name: z.string() }),
response: z.object({ id: z.number(), name: z.string() })
},
'@patch/users/:id': {
params: z.object({ id: z.number() }),
body: z.object({ name: z.string().optional() }),
response: z.object({ id: z.number(), name: z.string() })
},
'@delete/users/:id': {
params: z.object({ id: z.number() }),
response: z.object({ success: z.boolean() })
}
};
const apiFetch = createFetch(api, 'https://api.example.com');
// These two 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 created = await apiFetch('/users', { body: { name: 'John', email: 'john@example.com' } });
// Explicit methods
await apiFetch('@put/users/:id', { params: { id: 123 }, body: { name: 'Jane' } });
await apiFetch('@patch/users/:id', { params: { id: 123 }, body: { name: 'Jane Doe' } });
await apiFetch('@delete/users/:id', { params: { id: 123 } });Method Inference
The @method prefix is optional. Without it, what-the-fetch automatically uses POST for requests with a body and GET for requests without a body. Both /users/:id and @get/users/:id are equivalent and result in the same GET request.
Step-by-Step Tutorial
Let's build a complete type-safe API client using what-the-fetch:
Define Your API Schema
Start by defining schemas for all your API endpoints:
import { z } from 'zod';
const api = {
'/posts': {
// GET /posts - List posts with filters
query: z.object({
page: z.number().optional(),
limit: z.number().optional(),
tag: z.string().optional()
}),
response: z.array(z.object({
id: z.number(),
title: z.string(),
excerpt: z.string()
}))
},
'/posts/:id': {
// GET /posts/:id - Get post by ID
params: z.object({ id: z.number() }),
response: z.object({
id: z.number(),
title: z.string(),
content: z.string(),
author: z.string()
})
},
'/posts/create': {
// POST /posts/create - Create new post
body: z.object({
title: z.string(),
content: z.string()
}),
response: z.object({
id: z.number(),
title: z.string(),
content: z.string()
})
}
} as const;Create the Fetch Function
Use createFetch to build your typed API client:
import { createFetch } from 'what-the-fetch';
const apiFetch = createFetch(api, 'https://api.blog.com');You can also provide shared options that apply to all requests:
const apiFetch = createFetch(
api,
'https://api.blog.com',
{
headers: {
'Authorization': 'Bearer your-api-key'
}
}
);Make Type-Safe Requests
Now you can make fully typed requests with autocomplete:
// Fetch posts with query parameters
const posts = await apiFetch('/posts', {
query: { page: 2, limit: 20, tag: 'javascript' }
});
// posts is typed as Array<{ id: number; title: string; excerpt: string }>
// Fetch a single post
const post = await apiFetch('/posts/:id', {
params: { id: 42 }
});
// post is typed as { id: number; title: string; content: string; author: string }Add Custom Headers (Optional)
You can pass custom headers using the third parameter:
const post = await apiFetch(
'/posts/:id',
{ params: { id: 42 } },
{
headers: {
'Authorization': 'Bearer your-api-key',
'X-Custom-Header': 'value'
}
}
);Common Patterns
Using Different Schema Libraries
what-the-fetch works with any Standard Schema implementation:
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() })
}
};
const apiFetch = createFetch(api, 'https://api.example.com');import { createFetch } from 'what-the-fetch';
import * as v from 'valibot';
const api = {
'/users/:id': {
params: v.object({ id: v.number() }),
response: v.object({ id: v.number(), name: v.string() })
}
};
const apiFetch = createFetch(api, 'https://api.example.com');import { createFetch } from 'what-the-fetch';
import { type } from 'arktype';
const api = {
'/users/:id': {
params: type({ id: 'number' }),
response: type({ id: 'number', name: 'string' })
}
};
const apiFetch = createFetch(api, 'https://api.example.com');Combining Path and Query Parameters
Mix path parameters with 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 }
});
// URL: https://api.example.com/users/123/posts?limit=10&published=trueCustom Request Configuration
Pass additional fetch options as the third parameter:
const user = await apiFetch(
'/users/:id',
{ params: { id: 123 } },
{
headers: {
'Authorization': 'Bearer token',
'X-API-Key': 'your-key'
},
cache: 'no-cache',
signal: abortController.signal
}
);Reusing the Same Fetch Function
Create one fetch function and reuse it throughout your application:
// api.ts
export const apiFetch = createFetch(api, 'https://api.example.com');
// users.ts
import { apiFetch } from './api';
const user = await apiFetch('/users/:id', { params: { id: 123 } });
// posts.ts
import { apiFetch } from './api';
const posts = await apiFetch('/posts', { query: { limit: 10 } });Shared Configuration
Use shared configuration to set common options for all requests:
// Create fetch with shared headers and options
const apiFetch = createFetch(
api,
'https://api.example.com',
{
headers: {
'Authorization': 'Bearer my-token',
'X-API-Version': '2.0'
},
cache: 'no-cache'
}
);
// All requests will include the shared headers
const user = await apiFetch('/users/:id', { params: { id: 123 } });
// You can override or add headers per request
const post = await apiFetch(
'/posts/:id',
{ params: { id: 42 } },
{
headers: {
'X-Custom-Header': 'value' // Merged with shared headers
}
}
);Schema Validation Benefits
what-the-fetch validates both requests and responses automatically:
Parameterized Paths Require Schema
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.
const api = {
'/users': {
body: z.object({
name: z.string().min(3),
email: z.string().email()
}),
response: z.object({
id: z.number(),
name: z.string(),
email: z.string()
})
}
};
const apiFetch = createFetch(api, 'https://api.example.com');
// ✅ Valid request
const user = await apiFetch('/users', {
body: {
name: 'John Doe',
email: 'john@example.com'
}
});
// ❌ This would fail at runtime with validation error
try {
await apiFetch('/users', {
body: {
name: 'Jo', // Too short
email: 'invalid-email' // Invalid format
}
});
} catch (error) {
console.error('Validation failed:', error);
}Error Handling
what-the-fetch provides clear error messages for both validation and HTTP errors:
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');
try {
const user = await apiFetch('/users/:id', { params: { id: 123 } });
} catch (error) {
// Could be:
// 1. HTTP error (status !== 2xx)
// 2. Schema validation error
// 3. Network error
console.error('Request failed:', error);
}Handling HTTP Errors
what-the-fetch automatically throws on non-2xx status codes:
try {
const user = await apiFetch('/users/:id', { params: { id: 999 } });
} catch (error) {
if (error.message.includes('HTTP error')) {
console.error('Request failed with status:', error);
// Handle 404, 500, etc.
}
}Handling Validation Errors
Schema validation errors are thrown when response doesn't match schema:
try {
const user = await apiFetch('/users/:id', { params: { id: 123 } });
} catch (error) {
// Validation error if response doesn't match schema
console.error('Invalid response:', error);
}Type Safety
With TypeScript, most parameter errors are caught at compile time, preventing runtime errors before they happen!