npm.io
0.1.0 • Published 15h ago

@mockingbirdjs/next

Licence
MIT
Version
0.1.0
Deps
1
Size
129 kB
Vulns
0
Weekly
0
Mockingbird

Mockingbird

Turn production traffic into editable mocks

Mockingbird is a development tool that turns your real backend traffic into an editable, Git-friendly mock backend — powered by MSW. Browse your app while the backend is up; Mockingbird records every request and response into readable JSON fixtures. Then kill the backend, keep working. Edit a fixture, save, and the app reflects it instantly.

Record HTTP traffic into readable JSON fixtures, then work offline with auto-hot-reloading mocks — zero MSW boilerplate, fixtures as first-class development assets, and a mock backend that grows organically while you browse. Available as a Vite plugin (@mockingbirdjs/vite) and a Next.js plugin (@mockingbirdjs/next).

// vite.config.ts — this is the whole setup
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { mockingbird } from "@mockingbirdjs/vite";

export default defineConfig({
  plugins: [react(), mockingbird()],
});

Why

  • Mocks that write themselves. No hand-written MSW handlers, no fake-data generators drifting out of sync with production. The traffic you already generate every day is the mock data.
  • Backend down? VPN flaking? Staging broken? Rate-limited? Your frontend keeps working from fixtures.
  • Fixtures are code. Readable JSON, one file per response variant, reviewed in PRs, hot-reloaded on save.
  • Never a dead end. Hybrid mode serves what it knows and records what it doesn't — coverage grows as you browse.

Quick start

Vite

npm i -D @mockingbirdjs/vite msw

Add mockingbird() to your Vite plugins (see the snippet above), run vite, and click through your app once. That's it — no npx msw init, no worker file to commit, no handlers to write. Fixtures appear in mockingbird/fixtures/, and the Mockingbird pill in the corner shows what's happening (press alt+m).

Next.js

npm i -D @mockingbirdjs/next msw
// next.config.js
const { withMockingbird } = require("@mockingbirdjs/next");
module.exports = withMockingbird({});

Same deal: run next dev, browse your app, fixtures accumulate under mockingbird/fixtures/. The overlay works identically.

The three modes

Mode Network Fixtures written On miss Default
live Always Yes
hybrid On miss On miss Recorded
mock Never No 501

Switch modes from the overlay, the CLI (mockingbird mode mock), or the API — a running dev server picks changes up live, no restart. See Modes in depth for per-mode flowcharts and a full comparison.

Fixtures

Directories mirror your URL paths; one JSON file per method + variant. Dynamic segments (numeric ids, UUIDs, ULIDs) collapse into [id] folders:

mockingbird/
├── .gitignore                      # generated: ignores .state.json
└── fixtures/
    └── api/
        ├── users/GET.json
        ├── customers/
        │   ├── GET.json                # default variant
        │   ├── GET.page=2.json         # query-param variant
        │   └── [id]/
        │       ├── GET.json            # wildcard fallback — ANY id resolves
        │       └── GET.id=42.json      # exact instance
        └── orders/
            ├── POST.json               # fallback for any body
            └── POST.body-9f3c21ab.json # specific payload variant

The bare GET.json inside [id]/ is the wildcard fallback — it is written automatically the first time any variant of that endpoint is recorded. Any id you have never explicitly recorded resolves to this file instead of returning a miss, so mock mode never dead-ends on unseen values. The trade-off: all unrecorded ids return the same (first-seen) response. Disable with record: { wildcardFallback: false } when you need strict per-variant matching (e.g. per-customer data that must not bleed across ids), or scope it to a specific route:

routes: [
  { pattern: "/api/customers/[id]", record: { wildcardFallback: false } },
]

A fixture is plain JSON — edit response.body, save, refetch:

{
  "$schema": "https://mockingbird.dev/schema/fixture-v1.json",
  "request": { "method": "GET", "path": "/api/customers/[id]", "params": { "id": "42" } },
  "response": {
    "status": 200,
    "headers": { "content-type": "application/json; charset=utf-8" },
    "bodyType": "json",
    "body": { "id": 42, "name": "Ada Lovelace", "tier": "enterprise" },
    "latencyMs": 143
  },
  "meta": { "recordedAt": "2026-07-05T12:31:04Z" }
}

Hand-written fixtures only need request.method/request.path and response.status/response.body. Commit the whole directory.state.json (per-developer runtime mode) is auto-gitignored.

Security: sanitized by default

Nothing touches disk before the scrub pipeline runs, server-side:

  • Request headers are stored allowlist-only (content-type, accept).
  • set-cookie, authorization, www-authenticate, x-api-key, … are redacted (cookie names + attributes survive, values don't).
  • A JWT sweep catches tokens hiding anywhere in header values or JSON bodies.
  • Body fields matching **.password, **.token, **.accessToken, **.apiKey, **.secret, **.ssn, **.creditCard (configurable) are redacted before body hashing, so secrets never influence request matching.

That last point is why login still works in mock mode: matching ignores auth material entirely. Your app posts credentials (any password), gets the sanitized recorded response with a placeholder token, sends that placeholder on subsequent requests — and the matcher never looks at it.

Belt and braces: run mockingbird sanitize --check in CI or a pre-commit hook. It scans fixtures for secret-shaped values (JWTs, sk_live_…, AWS keys, bearer tokens, private key blocks) and exits non-zero on findings.

The overlay

A collapsed pill (HYBRID 12 3) shows the mode and live counters — served from fixture, forwarded, and (in mock mode) missed; click or press alt+m for the drawer:

  • Mode — one-click Live / Hybrid / Mock switch, persisted across restarts.
  • Requests — live log, each row tagged by kind (served from fixture · recorded · passthrough · miss), with Open in editor and Delete / re-record per row.
  • Endpoints — coverage: what's captured (and how many variants), what's been seen but not captured.
  • Stats — endpoint/fixture counts, session hit/miss, fixtures location.

Configuration

Zero-config works: hybrid mode, same-origin interception only, Vite internals and static assets excluded, secrets redacted. Everything is tunable:

mockingbird({
  mode: "hybrid",                    // 'live' | 'mock' | 'hybrid'
  dir: "mockingbird",                // fixture directory
  include: ["**"],                   // path globs to intercept
  ignore: ["/api/telemetry/**"],     // never intercept these
  backends: ["https://api.staging.acme.dev"], // cross-origin APIs to also handle
                                     // (all other cross-origin traffic passes through)
  match: {
    query: { ignore: ["utm_*"] },    // 'all' | 'none' | { only } | { ignore } — cache-busters always ignored
    body: true,                      // hash request bodies for POST/PUT variants
    inferPathParams: true,           // /customers/42 → /customers/[id]
  },
  record: {
    onDuplicate: "update",           // 'update' | 'keep' | 'variants'
    latency: true,                   // store observed latency in fixtures
    maxBodyBytes: 5_000_000,         // bigger responses become coverage stubs
    wildcardFallback: true,          // write GET.json catch-all for unseen ids/pages/bodies
  },
  replay: { latency: "none" },       // 'none' | 'recorded' | ms | { min, max }
  routes: [
    { pattern: "POST /api/auth/**", ignore: true },      // never intercept
    { pattern: "GET /api/flags", mode: "live" },         // always real
    { pattern: "/api/orders/[orderId]/items/[itemId]" }, // explicit path template
  ],
  sanitize: { bodyPaths: ["**.password", "user.ssn"] },
  overlay: { position: "bottom-right", hotkey: "alt+m" },
  onRecord: (ex) => (ex.response.status >= 500 ? null : ex), // drop 5xx recordings
})

The plugin is apply: 'serve' — it does not exist in production builds.

CLI

npx mockingbird status                    # mode, endpoint/fixture counts, disk size
npx mockingbird mode <live|mock|hybrid>   # running dev servers pick it up live
npx mockingbird list [glob]               # endpoints → fixtures
npx mockingbird clear [glob] --yes        # delete fixtures
npx mockingbird sanitize --check          # secret scan; non-zero exit for CI

Try the demo

Mockingbird ships two demo apps — a customer dashboard (Vite + Express) and a feature flag manager (Next.js) — both wired with deliberate 300–800 ms response delays and a generatedAt timestamp on every payload so you can see exactly when data was fetched vs. served from a fixture.

pnpm install && pnpm build

# Vite demo: starts a toy Express backend + Vite frontend
pnpm demo:vite

# Next.js demo
pnpm demo:nextjs

Vite demo walkthrough. Two terminals open: Express on one port, Vite on another. Click through the customer list — you'll see the delay and the ticking generatedAt. Mockingbird records every response. Now kill the Express terminal. Reload — the app keeps working, instantly, with frozen timestamps (served from fixtures, no network hop). Open any file under examples/basic/mockingbird/fixtures/, rename a customer, save: the change is live on the next request without a page reload.

Next.js demo walkthrough. One terminal running next dev. Browse the feature flags list — toggle flags, open details, check environment tags. Mockingbird records each API-route response. Switch to mock mode from the overlay (or mockingbird mode mock): subsequent requests are served from fixtures, timestamps freeze, and the API routes are never called. Edit a fixture, save, refetch — your change appears immediately.

How it works

flowchart LR
    fetch(["app fetch()"]) --> sw

    subgraph browser["Browser"]
        sw["MSW service worker"]
    end

    subgraph vite["Vite dev server"]
        resolve["/__mockingbird/resolve"]
        fixtures[("fixture index")]
        record["/__mockingbird/record"]
        pipeline["sanitize → dedupe\n→ atomic write → ws"]
    end

    sw -->|"known fixture"| resolve
    resolve --> fixtures
    fixtures -->|verbatim replay| app(["App ✓"])

    sw -->|"unknown / live"| bypass["fetch(bypass)"]
    bypass --> backend(["real backend"])
    backend --> clone{{"clone()"}}
    clone -->|"streamed copy"| app
    clone -->|"fire-and-forget"| record
    record --> pipeline
    pipeline --> overlay(["Overlay"])
  • Interception happens in the browser (one universal MSW handler); matching, storage, and sanitization live in the Vite dev server. The client caches nothing, which is why editing a JSON file is instantly consistent.
  • Recording never double-fetches: the real response is clone()d — one copy streams to your app immediately, the other is posted to the dev server without being awaited.
  • Entry scripts are gated on worker readiness via an HTML transform, so no request can slip to the network before interception is armed.
  • The MSW worker script is served from your installed msw package — nothing to commit to public/.
  • Identical re-recordings write nothing (no Git noise). Changed responses follow your onDuplicate policy.

Modes in depth

The mode controls what the MSW worker does with each request. Switch freely at runtime — a running dev server picks up changes instantly, no restart needed.

Live

In live mode the worker gets out of the way. Every request bypasses the fixture index and goes straight to the real backend. Responses are cloned and recorded as they arrive, building up fixture coverage while you browse normally. This is the right starting point for a new project or a new surface — you get real data and a growing fixture set at the same time.

flowchart TD
    req(["Request"]) --> sw["MSW worker"]
    sw -->|always| bypass["fetch(bypass)"]
    bypass --> be(["Real backend"])
    be --> clone{{"clone()"}}
    clone -->|stream| app(["App"])
    clone -->|record| fix[("Fixture index\ngrows")]
Mock

Mock mode severs the network entirely. Every request is matched against the fixture index; a hit is served instantly from disk, a miss returns a structured 501 Not Implemented — the network is never touched. This makes your frontend completely deterministic: same fixture file, same response, every time. It is the right mode for CI, for demos, and for any session where you want to guarantee that the backend cannot interfere.

flowchart TD
    req(["Request"]) --> sw["MSW worker"]
    sw --> hit{"fixture\nmatch?"}
    hit -->|yes| serve["Serve fixture"]
    hit -->|no| miss["501 Not Implemented"]
    serve --> app(["App"])
    miss --> app
Hybrid (default)

Hybrid mode combines the two. The worker checks the fixture index first; a known endpoint is served from disk instantly. An unknown endpoint is forwarded to the real backend, cloned, and recorded — then served to the app. The fixture set grows organically as you browse: the more you explore, the less you depend on the backend. This is the mode most teams live in day-to-day: fast and offline-capable for the surfaces already recorded, seamlessly live for anything new.

flowchart TD
    req(["Request"]) --> sw["MSW worker"]
    sw --> hit{"fixture\nmatch?"}
    hit -->|yes| serve["Serve fixture\n(instant)"]
    hit -->|no| bypass["fetch(bypass)"]
    bypass --> be(["Real backend"])
    be --> clone{{"clone()"}}
    clone -->|stream| app(["App"])
    clone -->|record| fix[("Fixture index\ngrows")]
    serve --> app
Comparison
Live Hybrid Mock
Network calls Always On miss Never
Fixtures read No Yes Yes
Fixtures written Yes On miss No
Backend required Yes No No
Response is deterministic No Partial Yes
Coverage grows while browsing Yes Yes No
On unknown endpoint Recorded Recorded 501
Best for Building coverage Day-to-day dev Offline · CI · demos

You can override the mode per-route via the routes config option — e.g. keep /api/flags always live while everything else runs from fixtures.

FAQ

Does this work with Next.js? Yes. Install @mockingbirdjs/next and wrap your Next.js config with withMockingbird({}). The core engine (@mockingbirdjs/core — store, matching, sanitization, middleware) has zero framework imports by design, so the Vite and Next.js adapters share the same matching logic, fixture format, and CLI.

I already use MSW. Mockingbird registers its own worker with onUnhandledRequest: 'bypass'. Running a second setupWorker in the same app is not supported yet — mixed manual-handler support is on the roadmap. For now, Mockingbird needs to be the sole MSW instance.

What about cookies? Browsers never expose Set-Cookie to JavaScript, so cookie values can't be captured from the page (they're also exactly what shouldn't be in fixtures). Cookie-based apps still work in mock mode because matching ignores cookies entirely — your app posts credentials, gets the sanitized recorded response with a placeholder token, and subsequent requests carry that placeholder without Mockingbird ever inspecting it.

Binary / huge / streaming responses? Binary bodies are stored as .bin sidecars and streamed on replay. Responses over maxBodyBytes become lightweight coverage stubs (status + headers, no body). SSE streams and WebSocket connections pass through unrecorded — replay support is on the roadmap.

Can I use this in CI? Yes — run in mock mode with a pre-committed fixture set. Add mockingbird sanitize --check to your pipeline to catch any secrets that slipped through before fixtures reach the remote. Use MOCKINGBIRD_MODE=mock if you prefer environment variables over config.

Roadmap

Version Theme Contents
v0.1 (current) Foundation Vite plugin, Next.js plugin, hybrid/live/mock modes, fixture recording & replay, sanitization pipeline, CLI, overlay
v0.2 Hygiene & control prune / doctor / export / import, variant pinning in the overlay, published fixture JSON schema
v0.3 Power workflows GraphQL operation-aware matching, scenario snapshots (scenario save demo), stale-fixture detection
v1.0 Team & realism Response templating ({{now}}, {{request.params.id}}), shared team fixtures, SSE/WebSocket record & replay

Repository layout

packages/core/                    # @mockingbirdjs/core — store, matching, sanitize, middleware (no Vite imports)
packages/mockingbird-vite/        # the vite plugin + browser client + overlay
packages/mockingbird-next/        # the next.js plugin + browser client + overlay
packages/cli/                     # the `mockingbird` binary
examples/basic/                   # vite demo app + toy backend
examples/nextjs/                  # next.js demo app
e2e/                              # Playwright: record → mock → hot-edit → hybrid, in a real browser

Develop: pnpm install && pnpm build && pnpm test && pnpm e2e. Lint: pnpm lint (Biome + Ultracite).

License

MIT

Keywords