Polly.js: Record and Replay HTTP in JavaScript Tests

Polly.js: Record and Replay HTTP in JavaScript Tests

JavaScript tests that make real HTTP requests are slow and unreliable. Manual mocks for fetch or axios require constant maintenance as APIs evolve. Polly.js solves both problems by recording real HTTP interactions and replaying them in subsequent test runs — giving you test speed and isolation without sacrificing the accuracy of real API responses.

Polly.js was originally built by Netflix engineers and open-sourced. It works with any JavaScript HTTP client and any test framework, and it supports multiple storage backends for saving recordings.

Core Concepts

Polly.js intercepts HTTP requests through adapters — pluggable modules that hook into specific HTTP clients. When you create a Polly instance, you tell it which adapters to use and where to store recordings.

The recording lifecycle:

  1. Record mode — requests go to the real server, response is saved
  2. Replay mode — requests are intercepted, saved response is returned
  3. Passthrough mode — requests go through unmodified (no recording, no replay)

Recordings are stored in HAR (HTTP Archive) format — a standard JSON format also used by browser developer tools — or in custom formats via persistence adapters.

Installation

npm install @pollyjs/core @pollyjs/adapter-node-http @pollyjs/persister-fs

Polly.js has a modular architecture. You install adapters for the HTTP clients you use (Node's built-in http/https, fetch, `XHR) and persisters for where recordings go (filesystem, REST API, in-memory).

For browser-based testing:

npm install @pollyjs/adapter-fetch @pollyjs/adapter-xhr @pollyjs/persister-rest

Basic Setup

import { Polly } from '@pollyjs/core';
import NodeHttpAdapter from '@pollyjs/adapter-node-http';
import FSPersister from '@pollyjs/persister-fs';

// Register adapters and persisters globally once
Polly.register(NodeHttpAdapter);
Polly.register(FSPersister);
// In your test
import { Polly } from '@pollyjs/core';

describe('GitHub API integration', () => {
  let polly;

  beforeEach(() => {
    polly = new Polly('GitHub User Fetch', {
      adapters: ['node-http'],
      persister: 'fs',
      persisterOptions: {
        fs: { recordingsDir: '__recordings__' }
      }
    });
  });

  afterEach(async () => {
    await polly.stop();
  });

  it('fetches a user profile', async () => {
    const response = await fetch('https://api.github.com/users/octocat');
    const data = await response.json();
    expect(data.login).toBe('octocat');
  });
});

The first run records https://api.github.com/users/octocat to __recordings__/GitHub User Fetch/record.har. Subsequent runs replay the saved response.

Recording Modes

Control recording behavior at the Polly instance level or per-route:

const polly = new Polly('My Suite', {
  mode: 'replay',        // Only replay, never record
  // mode: 'record'      // Always re-record
  // mode: 'passthrough' // Let all requests through
  // mode: 'stopped'     // Block all requests
});

For CI environments, replay mode is the right default — it ensures no network calls happen and tests fail clearly if a recording is missing rather than silently hitting the network.

Change the default globally for CI:

const mode = process.env.CI ? 'replay' : 'record';

const polly = new Polly('Test', { mode });

Intercepting and Modifying Requests

Polly.js provides a server API for intercepting requests and modifying behavior — useful for simulating error conditions without recording them from a real API:

const { server } = polly;

// Intercept and return a custom response
server.get('https://api.example.com/data').intercept((req, res) => {
  res.status(503).json({ error: 'Service unavailable' });
});

// Add a delay to simulate slow responses
server.get('https://api.example.com/slow').intercept((req, res, interceptor) => {
  interceptor.delay(2000);
  res.status(200).json({ result: 'ok' });
});

// Passthrough specific requests (don't record or replay)
server.get('https://api.example.com/realtime').passthrough();

This makes Polly.js useful as both a record-replay tool and a general HTTP mocking library.

Filtering Sensitive Headers and Bodies

API keys, auth tokens, and other sensitive data should not appear in committed recording files. Polly.js provides request and response event hooks for scrubbing sensitive data before it's stored:

const polly = new Polly('Auth Tests', {
  adapters: ['node-http'],
  persister: 'fs',
  persisterOptions: {
    fs: { recordingsDir: '__recordings__' }
  }
});

polly.server.any().on('beforePersist', (req, recording) => {
  // Remove auth headers from stored recording
  const { request } = recording;
  if (request.headers) {
    delete request.headers['authorization'];
    delete request.headers['x-api-key'];
  }
});

You can also normalize request data to make recordings more stable:

polly.server.any().on('request', (req) => {
  // Strip timestamp query params before matching
  req.query['timestamp'] = undefined;
});

Integration with Jest

Polly.js integrates cleanly with Jest using setup and teardown hooks in a shared helper:

// test-helpers/polly.js
import { Polly } from '@pollyjs/core';
import NodeHttpAdapter from '@pollyjs/adapter-node-http';
import FSPersister from '@pollyjs/persister-fs';

Polly.register(NodeHttpAdapter);
Polly.register(FSPersister);

export function setupPolly(testName) {
  let polly;

  beforeEach(() => {
    polly = new Polly(testName, {
      adapters: ['node-http'],
      persister: 'fs',
      mode: process.env.CI ? 'replay' : 'record',
      persisterOptions: {
        fs: { recordingsDir: 'src/__recordings__' }
      }
    });
  });

  afterEach(async () => {
    await polly.stop();
  });

  return { getPolly: () => polly };
}
// users.test.js
import { setupPolly } from '../test-helpers/polly';
import { fetchUser } from './users';

describe('fetchUser', () => {
  setupPolly('fetchUser');

  it('returns user data for valid username', async () => {
    const user = await fetchUser('octocat');
    expect(user.login).toBe('octocat');
    expect(user.id).toBeDefined();
  });
});

Handling HAR Files in Version Control

HAR recording files should be committed to your repository alongside tests. This makes them:

  • Reviewable — code review can catch API response changes
  • Reproducible — anyone cloning the repo can run tests immediately
  • Auditable — you can trace exactly what API response a test was written against

Add the recordings directory to git but not to .gitignore:

# .gitignore
# Do NOT ignore recordings
# __recordings__ ← don't add this

When an API changes and you need to update recordings, delete the relevant HAR file and run the test in record mode once. The updated recording shows up as a diff in your PR, making the API change visible.

Polly.js vs Manual Mocks

Manual jest.mock() calls for axios or fetch are common but fragile. They mock the module interface, not the actual HTTP behavior — so they break when you change HTTP clients, miss headers and status codes, and require you to invent response bodies that may not match what the real API returns.

Polly.js recordings come from the real API. The response structure is always accurate because it was real at some point. The tradeoff is that you need to run in record mode against a real environment at least once — which is usually the right constraint anyway.

For continuous production monitoring of the APIs your Polly.js tests cover, HelpMeTest provides 24/7 health checks that run against live endpoints and alert immediately when behavior changes — catching drift that replayed test recordings would never detect.

Read more

Start now free