Hierarchical Hooks
Add cross-cutting behavior at global, group, or endpoint levels with precise control over execution order.
Hook Levels
Hooks can be defined at three levels, each serving different purposes:
Global Hooks (Client-Level)
Apply to all requests across the entire API client:
const api = createApiClient({
users: group({
endpoints: {
list: get<void, User[]>('/users'),
}
}),
}, {
baseUrl: 'https://api.example.com',
hooks: {
beforeRequest: async (url, init) => {
// Runs for EVERY request
console.log('Making request to:', url);
return { url, init };
},
},
});Use global hooks for:
- Request/response logging
- Error tracking
- Performance monitoring
Group Hooks (Group-Level)
Apply to all endpoints within a group (including nested groups):
const api = createApiClient({
admin: group({
hooks: {
beforeRequest: async (url, init) => {
// Runs for all admin endpoints
return {
url,
init: {
...init,
headers: {
...init.headers,
'X-Admin-Request': 'true',
}
}
};
},
},
endpoints: {
stats: get<void, Stats>('/admin/stats'),
}
}),
}, {
baseUrl: 'https://api.example.com',
});Use group hooks for:
- Authentication
- Rate limiting per group
- Logging for specific API sections
- Adding context headers
Endpoint Hooks (Endpoint-Level)
Apply to a single specific endpoint:
const api = createApiClient({
users: group({
endpoints: {
create: post<CreateUserInput, User>(
'/users',
undefined,
{
beforeRequest: async (url, init) => {
// Runs only for user creation
console.log('Creating new user');
return { url, init };
},
afterResponse: async (response) => {
console.log('User created successfully');
return response;
},
}
),
}
}),
}, {
baseUrl: 'https://api.example.com',
});Use endpoint hooks for:
- Endpoint-specific validation
- Special logging or analytics
- Custom retry logic
- Response transformation
Hook Types
beforeRequest - Modify Requests
Called before the request is sent. Can modify URL and request options:
const api = createApiClient({
users: group({
endpoints: {
list: get<void, User[]>('/users'),
}
}),
}, {
baseUrl: 'https://api.example.com',
hooks: {
beforeRequest: async (url, init) => {
// Add authentication
const token = localStorage.getItem('jwt');
return {
url,
init: {
...init,
headers: {
...init.headers,
Authorization: `Bearer ${token}`,
}
}
};
},
},
});Signature:
beforeRequest?: (
url: string,
init: RequestInit
) => Promise<{ url: string; init: RequestInit }> | { url: string; init: RequestInit };afterResponse - Transform Responses
Called after receiving a response. Can modify the response:
const api = createApiClient({
users: group({
endpoints: {
list: get<void, User[]>('/users'),
}
}),
}, {
baseUrl: 'https://api.example.com',
hooks: {
afterResponse: async (response, url, init) => {
// Log response status
console.log(`${init.method} ${url} - ${response.status}`);
// Handle token refresh
if (response.status === 401) {
const newToken = await refreshToken();
localStorage.setItem('jwt', newToken);
// Retry with new token
return fetch(url, {
...init,
headers: {
...init.headers,
Authorization: `Bearer ${newToken}`,
},
});
}
return response;
},
},
});Signature:
afterResponse?: (
response: Response,
url: string,
init: RequestInit
) => Promise<Response> | Response;onError - Handle Errors
Called when an error occurs during the request:
const api = createApiClient({
users: group({
endpoints: {
list: get<void, User[]>('/users'),
}
}),
}, {
baseUrl: 'https://api.example.com',
hooks: {
onError: async (error) => {
// Log to error tracking service
console.error('API Error:', error);
// Could integrate with Sentry, Datadog, etc.
// Sentry.captureException(error);
},
},
});Signature:
onError?: (error: unknown) => Promise<void> | void;Hook Execution Order
Hooks execute in a specific order to provide predictable behavior:
beforeRequest Order (Sequential)
Executes from outer to inner:
- Global hooks
- Parent group hooks
- Child group hooks
- Endpoint-specific hooks
const api = createApiClient({
admin: group({
hooks: {
beforeRequest: async (url, init) => {
console.log('2. Admin group');
return { url, init };
},
},
groups: {
users: group({
hooks: {
beforeRequest: async (url, init) => {
console.log('3. Users subgroup');
return { url, init };
},
},
endpoints: {
create: post<CreateUserInput, User>(
'/admin/users',
undefined,
{
beforeRequest: async (url, init) => {
console.log('4. Endpoint');
return { url, init };
},
}
),
}
}),
}
}),
}, {
baseUrl: 'https://api.example.com',
hooks: {
beforeRequest: async (url, init) => {
console.log('1. Global');
return { url, init };
},
},
});
// Calling api.admin.users.create() logs:
// 1. Global
// 2. Admin group
// 3. Users subgroup
// 4. EndpointafterResponse Order (Reverse)
Executes from inner to outer (reverse order):
- Endpoint-specific hooks
- Child group hooks
- Parent group hooks
- Global hooks
const api = createApiClient({
admin: group({
hooks: {
afterResponse: async (response) => {
console.log('3. Admin group');
return response;
},
},
groups: {
users: group({
hooks: {
afterResponse: async (response) => {
console.log('2. Users subgroup');
return response;
},
},
endpoints: {
create: post<CreateUserInput, User>(
'/admin/users',
undefined,
{
afterResponse: async (response) => {
console.log('1. Endpoint');
return response;
},
}
),
}
}),
}
}),
}, {
baseUrl: 'https://api.example.com',
hooks: {
afterResponse: async (response) => {
console.log('4. Global');
return response;
},
},
});
// Calling api.admin.users.create() logs:
// 1. Endpoint
// 2. Users subgroup
// 3. Admin group
// 4. GlobalonError Order (Sequential)
Executes from outer to inner (same as beforeRequest):
- Global hooks
- Parent group hooks
- Child group hooks
- Endpoint-specific hooks
Common Use Cases
Authentication (Global)
Add authentication to all requests:
const api = createApiClient({
users: group({
endpoints: {
me: get<void, User>('/me'),
list: get<void, User[]>('/users'),
}
}),
}, {
baseUrl: 'https://api.example.com',
hooks: {
beforeRequest: async (url, init) => {
const token = localStorage.getItem('jwt');
return {
url,
init: {
...init,
headers: {
...init.headers,
Authorization: `Bearer ${token}`,
}
}
};
},
},
});Logging (Global/Group)
Log all requests and responses:
const api = createApiClient({
users: group({
endpoints: {
list: get<void, User[]>('/users'),
}
}),
}, {
baseUrl: 'https://api.example.com',
hooks: {
beforeRequest: async (url, init) => {
console.log(`→ ${init.method} ${url}`);
return { url, init };
},
afterResponse: async (response, url, init) => {
console.log(`← ${init.method} ${url} - ${response.status}`);
return response;
},
},
});Rate Limiting (Group)
Rate limit specific API sections:
const createRateLimiter = (requestsPerSecond: number) => {
let lastRequest = 0;
const minInterval = 1000 / requestsPerSecond;
return async (url: string, init: RequestInit) => {
const now = Date.now();
const timeSinceLastRequest = now - lastRequest;
if (timeSinceLastRequest < minInterval) {
await new Promise(resolve =>
setTimeout(resolve, minInterval - timeSinceLastRequest)
);
}
lastRequest = Date.now();
return { url, init };
};
};
const api = createApiClient({
analytics: group({
hooks: {
beforeRequest: createRateLimiter(2), // Max 2 requests/second
},
endpoints: {
track: post<Event, void>('/analytics/track'),
}
}),
users: group({
hooks: {
beforeRequest: createRateLimiter(10), // Max 10 requests/second
},
endpoints: {
list: get<void, User[]>('/users'),
}
}),
}, {
baseUrl: 'https://api.example.com',
});Error Tracking (Global)
Track all errors globally:
const api = createApiClient({
users: group({
endpoints: {
list: get<void, User[]>('/users'),
}
}),
}, {
baseUrl: 'https://api.example.com',
hooks: {
onError: async (error) => {
// Send to error tracking service
console.error('API Error:', error);
// Example integrations:
// Sentry.captureException(error);
// Datadog.logError(error);
// rollbar.error(error);
},
},
});Token Refresh (Global)
Automatically refresh expired tokens:
const api = createApiClient({
users: group({
endpoints: {
me: get<void, User>('/me'),
}
}),
}, {
baseUrl: 'https://api.example.com',
hooks: {
afterResponse: async (response, url, init) => {
// Handle 401 (except for auth endpoints)
if (response.status === 401 && !url.includes('/auth/')) {
try {
// Refresh the token
const refreshToken = localStorage.getItem('refreshToken');
const tokenResponse = await fetch('https://api.example.com/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
});
const { token } = await tokenResponse.json();
localStorage.setItem('jwt', token);
// Retry the original request with new token
return fetch(url, {
...init,
headers: {
...init.headers,
Authorization: `Bearer ${token}`,
},
});
} catch (error) {
// Refresh failed, redirect to login
window.location.href = '/login';
throw error;
}
}
return response;
},
},
});Conditional Headers (Group)
Add headers based on conditions:
const api = createApiClient({
admin: group({
hooks: {
beforeRequest: async (url, init) => {
const isAdmin = checkAdminStatus();
if (!isAdmin) {
throw new Error('Admin access required');
}
return {
url,
init: {
...init,
headers: {
...init.headers,
'X-Admin-Request': 'true',
'X-User-Role': 'admin',
}
}
};
},
},
endpoints: {
stats: get<void, Stats>('/admin/stats'),
}
}),
}, {
baseUrl: 'https://api.example.com',
});Next Steps
Master hooks and move on to:
- Configuration - Explore all client configuration options
- Plugins - Use plugins for reusable hook logic
- Error Handling - Combine hooks with error strategies
- Advanced Examples - Complex hook compositions