Overview
CallApi provides comprehensive lifecycle hooks that let you intercept and customize every stage of the request/response cycle. Hooks are asynchronous functions that receive context about the current request.
Available Hooks
CallApi provides 11 lifecycle hooks organized by execution stage:
Request Lifecycle
onRequest Called before the HTTP request is sent. Modify request headers, add authentication, or log requests. Source: hooks.ts:32-44
onRequestReady Called after request processing, just before sending. Final chance to inspect the request. Source: hooks.ts:59-63
onRequestStream Called during upload progress tracking. Useful for progress bars. Source: hooks.ts:65-76
onRequestError Called when network-level errors occur (timeouts, connection failures). Source: hooks.ts:47-56
Response Lifecycle
onResponse Called for all HTTP responses, regardless of status code. Source: hooks.ts:78-89
onSuccess Called for successful responses (2xx status codes). Source: hooks.ts:129-140
onResponseError Called for HTTP error responses (4xx, 5xx status codes). Source: hooks.ts:91-101
onResponseStream Called during download progress tracking. Useful for progress bars. Source: hooks.ts:103-114
Error & Retry Lifecycle
onError Unified error handler for all error types (network, HTTP, validation). Source: hooks.ts:22-32
onValidationError Called when request/response validation fails. Source: hooks.ts:142-153
Hook Context Types
Each hook receives a context object with relevant information:
RequestContext
Available in: onRequest, onRequestReady, onRequestStream
interface RequestContext {
baseConfig : BaseCallApiConfig ; // Base configuration
config : CallApiConfig ; // Instance configuration
options : CallApiExtraOptions ; // Merged options
request : CallApiRequestOptions ; // Mutable request object
}
Source: hooks.ts:177-213
SuccessContext
Available in: onSuccess
interface SuccessContext extends RequestContext {
data : TData ; // Parsed response data
response : Response ; // HTTP response object
}
Source: hooks.ts:223-227
ErrorContext
Available in: onError, onRequestError, onResponseError, onValidationError
interface ErrorContext extends RequestContext {
error : HTTPError | ValidationError | Error ;
response : Response | null ; // Available for HTTP errors
}
Source: hooks.ts:251-255
RetryContext
Available in: onRetry
interface RetryContext extends ErrorContext {
retryAttemptCount : number ; // Current retry attempt
}
Source: hooks.ts:263-268
StreamContext
Available in: onRequestStream, onResponseStream
interface StreamContext extends RequestContext {
event : StreamProgressEvent ;
requestInstance ?: Request ; // For upload
response ?: Response ; // For download
}
interface StreamProgressEvent {
loaded : number ; // Bytes transferred
total : number ; // Total bytes
progress : number ; // Percentage (0-100)
}
Source: hooks.ts:270-282
Basic Usage
Global Hooks
Define hooks in the base configuration:
import { createFetchClient } from 'callapi' ;
const callApi = createFetchClient ({
baseURL: 'https://api.example.com' ,
// Called before every request
onRequest : ({ request , options }) => {
console . log ( `Making request to: ${ options . fullURL } ` );
},
// Called for successful responses
onSuccess : ({ data , response }) => {
console . log ( 'Request succeeded:' , response . status );
},
// Called for error responses
onError : ({ error , response }) => {
console . error ( 'Request failed:' , error . message );
},
});
Per-Request Hooks
Override or extend hooks for specific requests:
const { data } = await callApi ( '/users' , {
onRequest : ({ request }) => {
console . log ( 'Custom request hook' );
},
onSuccess : ({ data }) => {
console . log ( 'Got user data:' , data );
},
});
Common Use Cases
Authentication
Add authentication headers:
const callApi = createFetchClient ({
baseURL: 'https://api.example.com' ,
onRequest : async ({ request }) => {
const token = await getAuthToken ();
request . headers . Authorization = `Bearer ${ token } ` ;
},
});
Request Logging
Log all requests with timing:
const callApi = createFetchClient ({
baseURL: 'https://api.example.com' ,
onRequest : ({ options }) => {
console . log ( `[ ${ new Date (). toISOString () } ] ${ options . fullURL } ` );
},
onSuccess : ({ response , options }) => {
const duration = performance . now () - options . meta ?. startTime ;
console . log ( `✓ ${ options . fullURL } ( ${ duration } ms)` );
},
});
Error Tracking
Send errors to a tracking service:
import * as Sentry from '@sentry/browser' ;
const callApi = createFetchClient ({
baseURL: 'https://api.example.com' ,
onError : ({ error , options , request }) => {
Sentry . captureException ( error , {
tags: {
url: options . fullURL ,
method: request . method ,
},
extra: {
requestId: options . meta ?. requestId ,
},
});
},
});
Upload Progress
Track file upload progress:
const fileInput = document . querySelector ( 'input[type="file"]' );
const progressBar = document . querySelector ( '.progress-bar' );
const formData = new FormData ();
formData . append ( 'file' , fileInput . files [ 0 ]);
await callApi ( '/upload' , {
method: 'POST' ,
body: formData ,
onRequestStream : ({ event }) => {
progressBar . style . width = ` ${ event . progress } %` ;
console . log ( `Uploaded: ${ event . loaded } / ${ event . total } ` );
},
});
Download Progress
Track file download progress:
const { data } = await callApi ( '/download/large-file.zip' , {
responseType: 'blob' ,
onResponseStream : ({ event }) => {
updateProgressBar ( event . progress );
console . log (
`Downloaded: ${ ( event . loaded / 1024 / 1024 ). toFixed ( 2 ) } MB / ` +
` ${ ( event . total / 1024 / 1024 ). toFixed ( 2 ) } MB`
);
},
});
Retry Logging
Log retry attempts:
const callApi = createFetchClient ({
baseURL: 'https://api.example.com' ,
retryAttempts: 3 ,
onRetry : ({ error , retryAttemptCount , options }) => {
console . warn (
`Retry ${ retryAttemptCount } for ${ options . fullURL } ` ,
`Reason: ${ error . message } `
);
},
});
Response Caching
Cache successful responses:
const cache = new Map ();
const callApi = createFetchClient ({
baseURL: 'https://api.example.com' ,
onRequest : ({ options }) => {
const cached = cache . get ( options . fullURL );
if ( cached && Date . now () - cached . timestamp < 60000 ) {
// Return cached data if less than 1 minute old
throw new CacheHitError ( cached . data );
}
},
onSuccess : ({ data , options }) => {
cache . set ( options . fullURL , {
data ,
timestamp: Date . now (),
});
},
});
Validation Error Handling
Show user-friendly validation messages:
import { toast } from './toast' ;
const callApi = createFetchClient ({
baseURL: 'https://api.example.com' ,
onValidationError : ({ error }) => {
const messages = error . issues . map ( issue => issue . message );
toast . error ({
title: `Invalid ${ error . issueCause } ` ,
message: messages . join ( ', ' ),
});
},
});
Hook Arrays
Register multiple functions for the same hook:
const callApi = createFetchClient ({
baseURL: 'https://api.example.com' ,
// Multiple hooks executed in order
onRequest: [
({ request }) => {
// Add authentication
request . headers . Authorization = getToken ();
},
({ request , options }) => {
// Add request ID
request . headers [ 'X-Request-ID' ] = generateId ();
},
({ options }) => {
// Log request
logger . info ( `Request to ${ options . fullURL } ` );
},
],
});
Source: hooks.ts:156-161
Hook Execution Modes
Control how multiple hooks execute:
const callApi = createFetchClient ({
baseURL: 'https://api.example.com' ,
// Execute hooks in parallel (default)
hooksExecutionMode: 'parallel' ,
onRequest: [
async () => { /* Hook 1 */ },
async () => { /* Hook 2 */ },
async () => { /* Hook 3 */ },
],
});
parallel (default)All hooks execute simultaneously using Promise.all(). Provides better performance but hooks cannot depend on each other’s results. hooksExecutionMode : 'parallel'
sequential Hooks execute one by one in registration order. Use when hooks depend on each other. hooksExecutionMode : 'sequential' ,
onRequest : [
async ({ request }) => {
// Hook 1: Get token
const token = await fetchToken ();
request . headers . Authorization = `Bearer ${ token } ` ;
},
async ({ request }) => {
// Hook 2: Depends on token from Hook 1
const permissions = await validateToken (
request . headers . Authorization
);
request . headers [ 'X-Permissions' ] = permissions ;
},
]
Source: hooks.ts:163-175
Advanced Patterns
Conditional Hooks
Execute hooks based on conditions:
const callApi = createFetchClient ({
baseURL: 'https://api.example.com' ,
onRequest : ({ options , request }) => {
// Only add auth for protected routes
if ( options . fullURL . includes ( '/protected/' )) {
request . headers . Authorization = getToken ();
}
// Only log in development
if ( process . env . NODE_ENV === 'development' ) {
console . log ( 'Request:' , options . fullURL );
}
},
});
Hook Composition
Create reusable hook factories:
// Hook factory for authentication
function createAuthHook ( getToken : () => Promise < string >) {
return async ({ request } : RequestContext ) => {
const token = await getToken ();
request . headers . Authorization = `Bearer ${ token } ` ;
};
}
// Hook factory for logging
function createLoggerHook ( logger : Logger ) {
return ({ options } : RequestContext ) => {
logger . info ( `Request to ${ options . fullURL } ` );
};
}
// Compose hooks
const callApi = createFetchClient ({
baseURL: 'https://api.example.com' ,
onRequest: [
createAuthHook ( getAuthToken ),
createLoggerHook ( myLogger ),
],
});
Abort Requests
Cancel requests from hooks:
const callApi = createFetchClient ({
baseURL: 'https://api.example.com' ,
onRequest : ({ request , options }) => {
// Cancel if user is offline
if ( ! navigator . onLine ) {
request . signal ?. abort ();
throw new Error ( 'Device is offline' );
}
// Cancel if cache hit
const cached = getFromCache ( options . fullURL );
if ( cached ) {
request . signal ?. abort ();
return cached ;
}
},
});
Modify Response Data
Transform response data in hooks:
const callApi = createFetchClient ({
baseURL: 'https://api.example.com' ,
onSuccess : ({ data }) => {
// Normalize data structure
if ( Array . isArray ( data )) {
return data . map ( normalizeItem );
}
return normalizeItem ( data );
},
});
Modifying the returned value from hooks does not change the actual response data. Use response transformers or middleware for that purpose.
Best Practices
Hooks execute in the critical request path. Avoid slow operations: // ❌ Bad: Slow operation in hook
onRequest : async ({ request }) => {
// This delays every request!
await slowDatabaseQuery ();
request . headers [ 'X-Data' ] = result ;
}
// ✅ Good: Fast operation
onRequest : ({ request }) => {
request . headers [ 'X-Request-ID' ] = generateId ();
}
Don’t let hook errors crash your app: onRequest : ({ request }) => {
try {
request . headers . Authorization = getToken ();
} catch ( error ) {
console . error ( 'Failed to get token:' , error );
// Continue without auth
}
}
Leverage TypeScript for type-safe hooks: import type { RequestContext } from 'callapi' ;
const authHook = ({ request } : RequestContext ) => {
// TypeScript knows the shape of request
request . headers . Authorization = getToken ();
};
const callApi = createFetchClient ({
onRequest: authHook ,
});
Be careful with mutations: // ✅ Good: Modify request headers
onRequest : ({ request }) => {
request . headers [ 'X-Custom' ] = 'value' ;
}
// ⚠️ Caution: External side effects
onRequest : ({ options }) => {
// This modifies external state
globalRequestCounter ++ ;
localStorage . setItem ( 'lastRequest' , options . fullURL );
}
Hook Execution Order
Hooks are called in the order they are registered. Plugin hooks execute before base config hooks, which execute before per-request hooks.