Skip to content

Super-simple way to record any API-calls made in unit tests and later replay the same payload in order to speed up integration tests.

License

Notifications You must be signed in to change notification settings

subtree/api-replay

Repository files navigation

πŸ“Ό api-replay

npm version Bun

A lightweight HTTP recording and replay library for Bun + TypeScript, designed to simplify and accelerate integration tests by recording real fetch responses and replaying them in future test runs.

api-replay helps you:

  • Record HTTP API calls made during integration tests
  • Replay those responses later, eliminating the need for live API access
  • Speed up test runs and improve reliability
  • Easily customize what parts of a request are considered for matching

Basic Usage

import { replayAPI } from 'api-replay';

const startTime = performance.now();
await replayAPI.start('my-first-call-recording',);

// You long-running call here
const response = await fetch("https://dummyjson.com/test?delay=1500");

const endTime = performance.now();
const timeTaken = endTime - startTime;

console.log(`Time taken: ${timeTaken.toFixed(1)}ms`);

await replayAPI.done();

Run using bun run file.ts. The second time you run this, it will be very fast since it replays the recorded response instead of making a live network call.

$ bun run basic-test.ts
Time taken: 1829.2ms

$ bun run basic-test.ts
Time taken: 2.2ms

More involved example

test('can read orders for a range of dates given day', async () => {
  await replayAPI.start('shopify client/can read orders for a range of dates given day', {
    debug: true, // Enable logging for this test
    include: {
      headers: ['Authorization'], // Include Authorization header in matching to support multiple users
    },
    exclude: {
      headers: ['Cookie'], // Ignore Cookie header to avoid issues with session-specific data
      query: ['token'], // Ignore token query param to avoid issues with session-specific data
    },
    recordingsDir: 'myapirecordings', // Custom directory for storing recordings
  });

  const client = new ShopifyClient();
  const orders = await client.getOrders('2024-08-26', '2024-08-28');
  expect(orders.length).toEqual(42);

  await replayAPI.done();
});

πŸ“¦ Installation

bun add -d api-replay

npm package: api-replay

Requirements:

  • Bun >=1.1.0

This library uses native Bun APIs (Bun.write, Bun.file, etc.) and requires Bun 1.1.0 or later due to improvements in fetch and Response handling.


🧩 Library API

await replayAPI.start(testName: string, config?: MatchingConfig): Promise<void>

Starts intercepting and recording or replaying API calls made via fetch.

  • testName is used to determine the filename for storing or reading recordings
  • Example: 'shopify client/can read orders for a range of dates given day'
  • Becomes: ./.api-replay/shopify-client--can-read-orders-for-a-range-of-dates-given-day.json
  • config (optional): controls which parts of the request are included or excluded from match comparison

await replayAPI.done(): Promise<void>

Stops interception and saves recordings (if in record mode).

πŸ”§ Debug Logging

Logging is disabled by default for clean test output. Enable it via:

Option 1: Environment Variable

APIREPLAYLOGS=1 bun test  

Option 2: Config Option

await replayAPI.start('test-name', { debug: true });

πŸ” Detailed Error Information

When no matching recording is found during replay, api-replay provides detailed error information to help you understand why the match failed. This includes:

  • What was searched for: The exact request details (method, URL, headers, body)
  • What's available: All recorded calls that could potentially match
  • Why it didn't match: By comparing search criteria with available recordings

βš™οΈ Matching Behavior

βœ… Always matched:

  • HTTP method (GET, POST, etc.)
  • URL path (/orders, /posts/1)

βœ… By default:

  • Query string (all keys)

❌ Not matched by default:

  • Headers
  • Failed responses (4xx, 5xx status codes)
  • Response headers (not recorded by default)

πŸ” When No Match is Found

If a request doesn't match any recorded calls during replay, api-replay throws an error with detailed information about:

  • The request that was being searched for
  • All available recorded calls
  • The specific differences that caused the mismatch

This helps you quickly identify why the match failed and configure the appropriate exclusions or inclusions.

βœ… Configurable via:

type MatchingConfig = {
  include?: {
    headers?: string[];
  };
  exclude?: {
    headers?: string[];
    query?: string[];
    body?: boolean;
  };
  debug?: boolean; // Enable logging for this session
  recordingsDir?: string; // Directory for storing recordings (default: '.api-replay')
  recordFailedResponses?: boolean; // Record and match failed responses (4xx, 5xx) (default: false)
  recordResponseHeaders?: string[] | "*"; // Response headers to record (default: none)
};

Examples:

// Match specific headers
{ include: { headers: ['Authorization'] } }

// Ignore some volatile query params
{ exclude: { query: ['timestamp', 'token'] } }

// Don't match on body
{ exclude: { body: true } }

// Enable debug logging for this test
{ debug: true }

// Use custom recordings directory
{ recordingsDir: 'my-recordings' }

// Use absolute path for recordings
{ recordingsDir: '/tmp/api-recordings' }

// Record and match failed responses
{ recordFailedResponses: true }

// Record all response headers
{ recordResponseHeaders: "*" }

// Record specific response headers only
{ recordResponseHeaders: ['content-type', 'x-api-version'] }

// Combine options
{ 
  debug: true,
  recordingsDir: 'custom-recordings',
  recordFailedResponses: true,
  recordResponseHeaders: ['content-type'],
  exclude: { headers: ['user-agent'], query: ['timestamp'] }
}

πŸ“‹ Response Header Recording

By default, api-replay does not record response headers to keep recordings lightweight and avoid issues with volatile headers like timestamps or server-specific values.

Default Behavior

await replayAPI.start('my-test');

const response = await fetch('/api/data');
// Response headers are not recorded in the JSON file

Recording All Response Headers

await replayAPI.start('my-test', {
  recordResponseHeaders: "*"
});

const response = await fetch('/api/data');
// All response headers (content-type, date, server, etc.) are recorded

Recording Specific Response Headers

await replayAPI.start('my-test', {
  recordResponseHeaders: ['content-type', 'x-api-version', 'cache-control']
});

const response = await fetch('/api/data');
// Only content-type, x-api-version, and cache-control headers are recorded

Note: Response header names are case-insensitive. Headers are stored in lowercase in the recording files. See https://datatracker.ietf.org/doc/html/rfc7230#section-3.2


🚫 Failed Response Handling

By default, api-replay only records and matches successful responses (2xx and 3xx status codes). Failed responses (4xx and 5xx) are ignored to improve test reliability and avoid brittle tests that depend on specific error conditions.

Default Behavior

await replayAPI.start('my-test');

// These responses are recorded and matched:
await fetch('/api/users/1'); // 200 OK βœ…
await fetch('/api/redirect'); // 301 Moved βœ…

// These responses are ignored:
await fetch('/api/nonexistent'); // 404 Not Found ❌
await fetch('/api/server-error'); // 500 Internal Server Error ❌

Recording Failed Responses

If you need to test error handling scenarios, enable recordFailedResponses:

await replayAPI.start('error-handling-test', {
  recordFailedResponses: true
});

// Now all responses are recorded and matched:
await fetch('/api/users/1'); // 200 OK βœ…
await fetch('/api/nonexistent'); // 404 Not Found βœ…
await fetch('/api/server-error'); // 500 Internal Server Error βœ…

πŸ“‚ File Naming Convention

  • By default, recordings are saved under: ./.api-replay/
  • Directory can be customized using the recordingsDir configuration option
  • Filename is derived from the test name by replacing slashes and spaces:
shopify client/can read orders for a range of dates given day
=>
shopify-client--can-read-orders-for-a-range-of-dates-given-day.json

πŸ—ƒ JSON File Format

Each recording file is a pretty-printed .json with this structure:

Note: Response headers are empty by default. Use recordResponseHeaders configuration to record specific headers or all headers.

{
  "meta": {
    "recordedAt": "2025-07-17T12:34:56Z",
    "testName": "shopify client/can read orders for a range of dates given day",
    "replayAPIVersion": "1.0.0"
  },
  "calls": [
    {
      "request": {
        "method": "GET",
        "url": "https://api.example.com/orders?startDate=2022-01-01",
        "headers": {
          "Authorization": "Bearer xyz"
        },
        "body": null
      },
      "response": {
        "status": 200,
        "headers": {},
        "body": "[{\"orderId\":1234}]"
      }
    }
  ]
}

πŸ”§ Troubleshooting

Common Issues

"No matching recorded call found" Error

When you see this error, the detailed output will show you exactly what was searched for and what's available. Common solutions:

  1. Headers differ: Use exclude: { headers: ['authorization', 'user-agent'] } to ignore volatile headers
  2. Query params differ: Use exclude: { query: ['timestamp', 'token'] } to ignore dynamic parameters
  3. Body differs: Use exclude: { body: true } if the body content varies between runs
  4. Method/URL differs: Check that your request matches exactly what was recorded

Example Fix:

// If you see authorization headers differ in the error output:
await replayAPI.start('my-test', {
  exclude: {
    headers: ['authorization', 'x-api-key'],
    query: ['timestamp']
  }
});

Debug Mode

Enable debug logging to see what's happening:

await replayAPI.start('my-test', { debug: true });
// or
APIREPLAYLOGS=1 bun test

πŸ” Security Note

Recorded headers and bodies are stored in plaintext. Be careful when recording:

  • Authorization
  • Cookies
  • Personally identifiable information (PII)

πŸ“Ž Roadmap Ideas

Future improvements (not in scope of v1):

  • Header redaction or masking
  • Expiration of recordings
  • Request/response transformers
  • Snapshot diffing and versioning

🀝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Development Setup

  1. Clone the repository
  2. Install dependencies: bun install
  3. Run tests: bun test
  4. Run type checking: bun run typecheck
  5. Build the project: bun run build

Code Quality

This project uses:

  • ESLint for code linting
  • Prettier for code formatting
  • Husky for pre-commit hooks
  • TypeScript for type safety

All contributions should pass the existing test suite and maintain 100% type coverage.


πŸ“„ License

MIT

About

Super-simple way to record any API-calls made in unit tests and later replay the same payload in order to speed up integration tests.

Topics

Resources

License

Stars

Watchers

Forks

Contributors 4

  •  
  •  
  •  
  •