Skip to Content
✨ New: Auth Plugin. JWT, OAuth2, API Key, and more authentication strategies out of the box. Read more... 🎉
Custom Handlers

Custom Handlers

Override the default JSON fetch behavior for special cases.

When to Use

Use custom handlers for:

  • File uploads — FormData instead of JSON
  • File downloads — Blob / ArrayBuffer responses
  • Non-JSON APIs — XML, plain text, etc.
  • Response transformation — Reshape data before returning

For most use cases (auth, logging, retries), prefer hooks or plugins.

Handler Signature

The handler is passed as the second argument to any endpoint helper:

get<TInput, TOutput>(path, handler, hooks?)

The handler receives:

async (context: { input: TInput; // The input passed to the endpoint call fetch: typeof fetch; // ⚠️ Always use this — not window.fetch — to preserve hooks method: HttpMethod; path: string; baseUrl: string; }) => Promise<TOutput>

File Upload

const api = createApiClient({ upload: post<{ file: File; name: string }, { id: string; url: string }>( '/files', async ({ input, fetch, baseUrl, path }) => { const formData = new FormData(); formData.append('file', input.file); formData.append('name', input.name); // Do NOT set Content-Type manually — the browser adds it with the correct boundary const response = await fetch(`${baseUrl}${path}`, { method: 'POST', body: formData }); if (!response.ok) throw { status: response.status, statusText: response.statusText, error: await response.json() }; return response.json(); } ), }, { baseUrl: 'https://api.example.com' }); const result = await api.upload({ file: myFile, name: 'report.pdf' }); console.log(result.url);

File Download

const api = createApiClient({ download: get<{ id: string }, Blob>( (input) => `/files/${input.id}`, async ({ fetch, baseUrl, path }) => { const response = await fetch(`${baseUrl}${path}`, { method: 'GET' }); if (!response.ok) throw { status: response.status, statusText: response.statusText, error: null }; return response.blob(); } ), }, { baseUrl: 'https://api.example.com' }); const blob = await api.download({ id: 'file-123' }); const url = URL.createObjectURL(blob);

Response Transformation

Transform or reshape the API response before returning:

const api = createApiClient({ getUser: get<{ id: string }, User>( (input) => `/users/${input.id}`, async ({ fetch, baseUrl, path }) => { const response = await fetch(`${baseUrl}${path}`, { method: 'GET' }); const raw = await response.json(); // Rename snake_case → camelCase return { id: raw.id, name: `${raw.first_name} ${raw.last_name}`, email: raw.email_address, createdAt: new Date(raw.created_at), }; } ), }, { baseUrl: 'https://api.example.com' });

Non-JSON Responses

const api = createApiClient({ getText: get<{ id: string }, string>( (input) => `/content/${input.id}`, async ({ fetch, baseUrl, path }) => { const response = await fetch(`${baseUrl}${path}`, { method: 'GET' }); if (!response.ok) throw { status: response.status, statusText: response.statusText, error: null }; return response.text(); } ), }, { baseUrl: 'https://api.example.com' });

Always use the fetch from the context object — it has all hooks applied. Using window.fetch or a global fetch directly bypasses hooks.