Hooks
Hooks intercept requests and responses at global, group, or endpoint level.
Hook Types
| Hook | Signature | Purpose |
|---|---|---|
beforeRequest | (url, init) => Promise<{ url, init }> | Modify or inspect the request before it’s sent |
afterResponse | (response, url, init) => Promise<Response> | Inspect or replace the response |
onError | (error) => Promise<void> | Handle or log errors |
const api = createApiClient({ /* endpoints */ }, {
baseUrl: 'https://api.example.com',
hooks: {
beforeRequest: async (url, init) => {
// Must return { url, init }
return { url, init: { ...init, headers: { ...init.headers, 'X-Request-ID': crypto.randomUUID() } } };
},
afterResponse: async (response, url, init) => {
// Must return a Response
return response;
},
onError: async (error) => {
// No return value
console.error('API error:', error);
},
},
});Hook Levels
Hooks can be defined at three levels — they all stack and run together:
| Level | Where | Applies to |
|---|---|---|
| Plugin | Plugin factory | All endpoints |
| Global | Client config hooks | All endpoints |
| Group | group({ hooks: ... }) | All endpoints in the group and its sub-groups |
| Endpoint | Third argument of get(), post(), etc. | That endpoint only |
const api = createApiClient({
admin: group({
hooks: {
// Runs for all endpoints inside 'admin'
beforeRequest: async (url, init) => ({ url, init: { ...init, headers: { ...init.headers, 'X-Admin': 'true' } } }),
},
endpoints: {
createUser: post<Input, Output>(
'/admin/users',
undefined,
{
// Runs only for createUser
beforeRequest: async (url, init) => { console.log('creating user'); return { url, init }; },
}
),
},
}),
}, {
baseUrl: 'https://api.example.com',
hooks: {
// Runs for every request
beforeRequest: async (url, init) => ({ url, init }),
},
});Execution Order
For beforeRequest: plugin → global → outer group → inner group → endpoint
For afterResponse: endpoint → inner group → outer group → global → plugin (reverse)
onError follows the same order as beforeRequest.
Common Patterns
Authentication
hooks: {
beforeRequest: async (url, init) => ({
url,
init: {
...init,
headers: { ...init.headers, Authorization: `Bearer ${localStorage.getItem('jwt')}` },
},
}),
}Token Refresh on 401
hooks: {
afterResponse: async (response, url, init) => {
if (response.status === 401 && !url.includes('/auth/')) {
const token = await refreshToken();
localStorage.setItem('jwt', token);
return fetch(url, { ...init, headers: { ...init.headers, Authorization: `Bearer ${token}` } });
}
return response;
},
}Logging
hooks: {
beforeRequest: async (url, init) => {
console.log(`→ ${init.method} ${url}`);
return { url, init };
},
afterResponse: async (response, url, init) => {
console.log(`← ${response.status} ${init.method} ${url}`);
return response;
},
}