Whisper

Middleware

Intercept requests and responses with the middleware pipeline

Overview

Middleware lets you hook into every API call made through a Whisper client. Each middleware can inspect or modify the request before it is sent and the response after it returns. Use middleware for logging, metrics, or request/response transformation.

Creating Middleware

A middleware is an object with optional onRequest and onResponse hooks:

import type { Middleware } from '@wardbox/whisper/core';

const logger: Middleware = {
  name: 'logger',

  onRequest(context) {
    console.log(`-> ${context.method} ${context.url}`);
    return context;
  },

  onResponse(response, context) {
    console.log(`<- ${response.status} ${context.url}`);
    return response;
  },
};

Both hooks can be synchronous or async. Return the (possibly modified) context or response.

Registering Middleware

Pass an array of middleware to createClient. They execute in registration order:

import { createClient } from '@wardbox/whisper/core';

const client = createClient({
  apiKey: 'RGAPI-your-key-here',
  middleware: [logger, metrics, customRetry],
});

Execution Order

Middleware follows an onion model:

  • onRequest hooks run forward -- first registered middleware runs first
  • onResponse hooks run reverse -- first registered middleware runs last
Request flow:   logger.onRequest -> metrics.onRequest -> [API call]
Response flow:  metrics.onResponse -> logger.onResponse

This means the outermost middleware (first in the array) wraps the entire pipeline. A timing middleware registered first would measure the total time including all inner middleware.

Use Cases

Request Timing

const timer: Middleware = {
  name: 'timer',

  onRequest(context) {
    (context as any)._startTime = Date.now();
    return context;
  },

  onResponse(response, context) {
    const duration = Date.now() - (context as any)._startTime;
    console.log(`${context.methodId} took ${duration}ms`);
    return response;
  },
};

Custom Headers

const customHeaders: Middleware = {
  name: 'custom-headers',

  onRequest(context) {
    return {
      ...context,
      headers: {
        ...context.headers,
        'X-Custom-Source': 'my-app',
      },
    };
  },
};

Error Logging

const errorLogger: Middleware = {
  name: 'error-logger',

  onResponse(response, context) {
    if (response.status >= 400) {
      console.error(
        `API error: ${response.status} on ${context.method} ${context.url}`,
      );
    }
    return response;
  },
};

Request Context

The RequestContext object passed to onRequest contains:

PropertyTypeDescription
urlstringFull request URL
methodstringHTTP method (GET, POST, etc.)
headersRecord<string, string>Request headers
bodystring | undefinedRequest body (POST/PUT)
routePlatformRoute | RegionalRouteRouting value for this request
methodIdstringAPI method identifier (e.g., 'summoner-v4.getByPuuid')

Response Object

The ApiResponse object passed to onResponse contains:

PropertyTypeDescription
dataTParsed response body
statusnumberHTTP status code
headersRecord<string, string>Response headers

Pipeline Position

Middleware runs after the cache check and around the rate limiter and HTTP request:

Cache hit? -> return cached
Cache miss -> onRequest -> rate limit -> fetch -> onResponse -> cache store

Cached responses skip middleware entirely for performance. If you need middleware to run on every call regardless of cache, disable caching for those endpoints with cacheTtl: { '/path': 0 }.

On this page