Skip to main content

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

onRetry

Called before each retry attempt.Source: hooks.ts:116-127

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'
sequentialHooks 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.

Build docs developers (and LLMs) love