npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@avivbenhamo/polly-kit

v0.9.0

Published

Drop-in record/replay of an app's HTTP traffic with Polly.JS, plus the polly-studio editor. Bring your own host policy.

Readme

@avivbenhamo/polly-kit

Record an app's real HTTP traffic to HAR files, then replay it so the app runs with no backends. Polly.JS, the recording store and the HAR editor in one package — one install, no companion to keep in step.

Entry points:

| | | |---|---| | @avivbenhamo/polly-kit | a Vite app, in the browser | | @avivbenhamo/polly-kit/node | a Node process — NestJS, a service, a CLI. http(s) and the built-in fetch | | @avivbenhamo/polly-kit/studio | the editor's handler and HAR API, to mount anywhere | | @avivbenhamo/polly-kit/studio/browser | the editor for a browser app with no server at all | | @avivbenhamo/polly-kit/vite | the Vite plugin | | @avivbenhamo/polly-kit/store | the recording store as bare connect middleware |

Nothing in here knows a hostname. You supply a policy; the package is the machinery.

Install

npm i -D @avivbenhamo/polly-kit

Three steps

1. vite.config.ts — one plugin, dev-server only (it registers in configureServer, so a production build never sees it):

import pollyKit from '@avivbenhamo/polly-kit/vite';

export default defineConfig({
    plugins: [react(), pollyKit({recordingsDir: 'recordings'})],
});

2. A config file — the only file you write:

// src/polly/config.ts
import type {PollyPolicyConfig} from '@avivbenhamo/polly-kit';

export const pollyConfig: PollyPolicyConfig = {
    recordingName: 'my-app',
    policy: {kind: 'except', hosts: [import.meta.env.VITE_AUTH_URL]},
};

3. A bootstrap module, imported FIRST in your entry point:

// src/polly/bootstrap.ts
import {resolveMode, startPolly} from '@avivbenhamo/polly-kit';
import {pollyConfig} from './config';

const mode = import.meta.env.PROD ? 'off' : resolveMode(import.meta.env.VITE_POLLY_MODE);

startPolly({...pollyConfig, mode});
// src/index.tsx — FIRST import, above everything
import './polly/bootstrap';

bootstrap.ts exists so that startPolly() runs before your component tree is evaluated. ES module imports are hoisted and fully evaluated before any statement in the importing module's body, so a bare startPolly() call in index.tsx runs after ./App and everything it imports — and any module that fires a request at import time would escape interception, silently and only sometimes.

Add the recordings directory to .gitignore — a HAR holds complete response bodies:

npx polly-kit init                         # adds /recordings, or /apps/web/recordings
npx polly-kit init --dir fixtures/har      # when that is not where you record

Run it from the directory the app records in. The entry is anchored at the repository root and written out in full — /apps/web/recordings from a package inside a monorepo, not /recordings, which would match nothing there and look like it had. An entry already in the file, in any spelling git honours, is left alone; running it twice adds one line.

Running it

Off unless VITE_POLLY_MODE is set, so the default behaviour of your app is unchanged.

$env:VITE_POLLY_MODE='record'; npm run dev     # capture real traffic
$env:VITE_POLLY_MODE='replay'; npm run dev     # serve from recordings
VITE_POLLY_MODE=record npm run dev             # bash

Record once against reachable backends, then replay forever. Recordings land in recordings/<recordingName>_<hash>/recording.har, editable at http://localhost:/__polly — the URL is printed on startup.

Non-browser apps

@avivbenhamo/polly-kit/node is the same thing for a Node process — NestJS, a plain service, a CLI. It intercepts both transports: http(s).request/get (axios, got, Nest's HttpModule) and Node's built-in fetch.

// src/polly/mock.ts
import {startPolly, resolveMode} from '@avivbenhamo/polly-kit/node';

export const polly = startPolly({
    mode: resolveMode(process.env.POLLY_MODE),
    recordingName: 'my-api',
    recordingsDir: './recordings',
    policy: {kind: 'only', hosts: [process.env.PARTNER_API_URL]},
});
// main.ts
import {polly} from './polly/mock';               // FIRST import
import {pollyUi} from '@avivbenhamo/polly-kit/node';

// startPolly() runs above NestFactory.create: providers can fire requests during
// module init, and anything that goes out ahead of it is not recorded.
const app = await NestFactory.create(AppModule);

if (polly) {
    app.use('/__polly', pollyUi({recordingsDir: './recordings'}));
}

// Polly persists on stop. A SIGKILL still loses whatever the debounce had not flushed.
for (const signal of ['SIGINT', 'SIGTERM']) {
    process.on(signal, async () => {
        await polly?.stop();
        process.exit(0);
    });
}
$env:POLLY_MODE='record'; npm run start:dev

No Vite plugin and no recording store: persister-fs writes the HAR straight to disk. npx polly-kit init gitignores the directory.

Use {kind: 'only'}, not {kind: 'all'}. In a browser Polly sees the page's own requests. In Node the adapters are process-wide: they also see your service talking to Postgres, Redis, the broker, its health checks and its telemetry collector. {kind: 'all'} on a server mocks the database. startPolly logs the policy it resolved on startup — read that line once.

Call http.get(...) off the namespace, never import {get} from 'node:http'. A named ESM import of a builtin is bound before the adapter patches the module object, and the already-bound name never sees the patch. Measured on Node v22.14.0: every request completes against the real upstream and Polly records none of them. axios is unaffected — it resolves request off the module object at call time.

Streaming is buffered. Polly hands an adapter a complete response, so a server-sent-events or long-poll endpoint will not stream in record mode. Put such hosts in alwaysPassthrough.

Compressed responses are decompressed before they are stored, and their content-encoding and content-length go with the compression. The undici dispatcher sees bodies still gzipped — fetch decompresses in a layer above it — so storing them raw would put base64 of compressed bytes in the HAR and leave nothing editable in the editor.

Quieter than Polly's default. logLevel defaults to 'error' here, not Polly's 'warn': @pollyjs/adapter warns on every recorded request when 'navigator' in global && !navigator.onLine, and Node has a global navigator with no onLine — so the claim is always false, and each one drags a full stringified request and config into the log. Pass logLevel: 'warn' to get Polly's default back.

Requires Node >= 22.6. Node 24 ships undici 7, whose handler protocol differs; fetch interception then fails with a message naming your Node version rather than silently recording nothing.

Policies

policy: {kind: 'all'}                    // mock every host
policy: {kind: 'except', hosts: [...]}   // mock everything EXCEPT these
policy: {kind: 'only',   hosts: [...]}   // mock ONLY these

Hosts match by origin, so a trailing slash or a path on the value is fine — they are baseURLs in practice, and toOrigin reduces them. An entry that resolves to undefined (an env var that was never set) is skipped rather than throwing at startup.

What is never mocked differs by entry point. In the browser, the app's own origin is always passed through: it serves the HMR socket, the recording store and every static asset, so mocking it breaks the dev server. On ./node there is no such origin, so alwaysPassthrough starts empty — nothing is exempt until you name it. That is deliberate: the adapters are process-wide, and a default that guessed which of your hosts were infrastructure would guess wrong quietly.

Passed-through hosts still hit the real network in replay mode. That is what passthrough means, but it has a sharp edge: the hosts you exclude are usually auth and analytics, which are also the least likely to be reachable from an isolated environment. Replaying off-network then gives you hanging calls to exactly those while everything else replays fine — which reads as "Polly is broken" and isn't. For true isolation use {kind: 'all'} and record them too.

The editor

The editor ships inside this package — there is nothing else to install. On the Vite path the plugin mounts it for you at /__polly. Anywhere else, pollyUi() is a bare (req, res) handler:

import {pollyUi} from '@avivbenhamo/polly-kit/studio';

app.use('/__polly', pollyUi({recordingsDir: './recordings'}));

On a raw http.createServer also pass basePath: '/__polly', since nothing stripped the prefix for you. Options: recordingsDir (required), basePath, cwd, and enabled, which defaults to process.env.NODE_ENV !== 'production'.

Without an app

npx polly-kit studio --dir recordings      # http://127.0.0.1:7788

For a checkout with no dev server to mount on: reviewing the recordings in a pull request, pruning a stale one, opening somebody else's recordings/ without installing their app. Same editor, same files — the recordings directory is the only state, so an app recording into it from another process shows up without restarting anything. If the port is taken it walks up to the next free one and says so.

The mounted form is still the default, and the better one when there is a dev server: no second process, no second port, no CORS, and it stops when the server does. Recordings kept in a browser's localStorage cannot be served this way at all — no separate process can see them, which is what mountPollyStudio is for.

Unlike the mounted handler this ignores NODE_ENV. Something mounted into an app can be deployed by accident; an npx invocation cannot, and a shell that happens to export NODE_ENV=production would otherwise get 404s with no explanation.

It answers 404, not 403, when disabled — the same answer an unmounted path gives, rather than confirming something is there. Recordings hold real request and response bodies, so "someone mounted this and shipped it" must not become a data leak.

For a browser app with no server at all, recording through Polly's persister-local-storage, @avivbenhamo/polly-kit/studio/browser mounts the same UI against that one localStorage key:

import {mountPollyStudio} from '@avivbenhamo/polly-kit/studio/browser';

mountPollyStudio(document.getElementById('polly')!);

Importing a HAR

The editor takes a HAR downloaded from the swagger-recorder extension — the plain flavour, not the zip — and turns it into a recording. Give it the recording name your app's Polly config uses; it creates the recording if there is none, and merges into it if there is. It reports what it did: added 3, replaced 2, skipped 1.

Where a request is in both, the imported one wins. That is usually what you want — you re-recorded because the response changed — but it means an import can overwrite a response you edited here. The replaced count is there so that is never silent.

Only HARs from that extension can be imported, and the reason is not caution. Polly finds a recorded response by an _id that hashes the request body, and a multipart body is not in a HAR at all — an upload records with no postData. So no importer can derive an id; it can only read one that was written at capture time, and that extension is the only thing that writes one.

API

| Export | From | What | |---|---|---| | startPolly(options) | . | Starts Polly. Returns null when mode is 'off', having constructed nothing. | | resolveMode(raw) | . | Narrows an env value to 'off' \| 'record' \| 'replay'. Anything unrecognised is off. | | toOrigin / toOrigins | . | The origin reduction the policy matcher uses. | | pollyKit(options) | ./vite | The Vite plugin: recording store + studio. | | recordingStore(options) | ./store | The store as a bare connect middleware, for mounting on something that isn't Vite. | | startPolly / resolveMode | ./node | The Node entry: http(s) and fetch, HAR straight to disk. | | pollyUi(options) | ./node, ./studio | The editor as a bare (req, res) handler. Mounts on Express, Fastify, Koa, Vite or raw http. | | FsStore / serializeHar | ./studio | The HAR file store the editor reads and writes through. | | parseHar / findEntry | ./studio | Reading a HAR, with the Polly-specific quirks handled. | | applyResponsePatch / deleteEntry | ./studio | Editing an entry, returning a typed EditError rather than throwing. | | mountPollyStudio(root) | ./studio/browser | The editor with no server at all, against Polly's persister-local-storage. | | mergeEntries / rejectImport | ./studio | The import rules, as pure functions. | | guidForRecording(name) | ./studio | The directory Polly looks a recording up in. |

startPolly options beyond the policy config: mode (required), alwaysPassthrough, apiNamespace, saveDebounceMs; on ./node, recordingsDir and logLevel instead of apiNamespace. pollyKit options: recordingsDir, studioPath, apiNamespace, studio. Change apiNamespace in one place and you must change it in both.

Notes on why it is built this way

In the browser: both adapters. A browser has two request transports and an app can use either, so covering one covers half the app — silently, because an adapter attaches cleanly whether or not any traffic reaches it. adapter-xhr patches window.XMLHttpRequest, which is what axios uses (it resolves its adapter as ['xhr', 'http', 'fetch']), so no axios instance in your app needs to change. adapter-fetch patches window.fetch, which is what code written against the platform calls directly. They patch different globals and do not interact.

On Node: two adapters, one of them ours. @pollyjs/adapter-node-http patches http.request/http.get, which covers axios, got and Nest's HttpModule. It sees nothing of the built-in fetch: that is undici, which reads its dispatcher off a global symbol and opens a socket without going through http.request. Polly ships no adapter for that layer, so this package contains one. An http-only wrapper would attach cleanly and record none of your fetch traffic — silently, which is the failure this package exists to prevent.

The global shim. @pollyjs/persister-rest reads a bare, unbound global at module top level, which throws in a browser. startPolly's module imports the shim as its own first import, so you cannot get the order wrong.

Recordings flush while you browse. Polly only persists on stop(), which suits a test that ends; a dev server page stays open and would write nothing. The kit debounces a persist() per response instead.

Defaults that differ from Polly's. recordIfMissing: false (a replay miss must fail loudly — the default quietly hits the network and records it, which looks identical to replay working), recordFailedRequests: true, matchRequestsBy.order: false (screens refetch) and .headers: false (bearer tokens would pin a recording to one session).

If you also run MSW, Polly wins. It patches XMLHttpRequest and fetch in-page, so the request is answered before the service worker sees it. Run one or the other.