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

badmock

v0.1.0

Published

A zero-config, zero-dependency mock API that misbehaves like production. Shape any response on demand — and simulate a server under stress: rising latency, hanging connections, bad periods, and circuit breakers.

Readme

badmock

A mock API that's bad on purpose.

CI npm version zero dependencies node >= 18 license MIT

Every mock server gives you the happy path. badmock gives you production: latency that rises under load, requests that hang until your AbortController fires, bad minutes that come and go on their own, and a circuit breaker that trips and recovers. It's a stateful degradation simulation you point your frontend at — because the sad path is the part of your app you never get to test, until users find it for you.

Zero config, zero dependencies (~100 kB), nothing to import into your app — the interaction is pure HTTP. And underneath the chaos it's also a complete everyday mock API: shape any response on demand, serve fixed routes from a file.

See it in action

https://github.com/user-attachments/assets/46c6ce99-adbf-4b65-803c-cdcd04e45705

The page in the video is just a visual client — a plain HTML page sending ordinary HTTP requests to badmock running as npx badmock --simulate overloaded. Everything you see (the climbing latency, the failures, the recovery) is the server misbehaving; a frontend of your own pointed at the same URL experiences exactly the same thing.

| Button | What the server receives | | --- | --- | | 🔨 Hammer the API | 30 × plain GET /employees — nothing special about the requests; the rising latency is the server reacting to load on its own | | 📡 Single request | 1 × GET /employees | | ⚡ Force bad period | POST /api/sim/trigger with {"what":"bad_period","durationMs":10000} — 10 seconds of failures, hangs and amplified latency | | 🔄 Reset | POST /api/sim/reset — closes the circuit breaker, ends the bad period, releases hung connections |

A few things the video doesn't show:

  • Shape any response per requestcurl -H "X-Mock-Status: 404" -H "X-Mock-Delay: 800" http://localhost:3000/anything
  • Trip the circuit breaker on demandPOST /api/sim/trigger with {"what":"circuit_open"}
  • Other personalities--simulate flaky (frequent failures, touchy breaker) and --simulate unstable (hangs and network-style trouble)
  • Fixed routes + custom simulation from a badmock.config.json

Contents: Quick start · How it works · What badmock simulates · vs json-server / MSW · The mock API · Recipes · Installation · Configuration reference · CLI · Programmatic API · FAQ

Quick start

npx badmock --simulate overloaded
# ▶ badmock listening on http://127.0.0.1:3000
#   ⚡ simulation active (preset "overloaded")

Point your frontend at it and use it like any API:

const res = await fetch("http://localhost:3000/products");
// sometimes fast, then slower as you hammer it, sometimes 503,
// sometimes… nothing at all, until your timeout fires. Like real life.

Watch what the "server" is going through, live:

curl http://localhost:3000/api/sim/status
# → { "mode": "bad_period", "reqPerSec": 42, "addedLatencyMs": 2600,
#     "circuit": { "state": "open", … }, "hungRequests": 3, … }

Does your spinner ever resolve? Does your retry logic back off? Does anything tell the user? That's what badmock is for.

Just want a plain, well-behaved mock? npx badmock (no flag) gives you the everyday mock API with no chaos at all — and you can turn the chaos on later without changing a line of app code.

How it works

badmock runs as its own server, separate from your frontend dev server. Two processes side by side:

| | What it is | Typically on | | --- | --- | --- | | Your frontend | Vite / Next.js / CRA dev server | localhost:5173 | | badmock | this mock server | localhost:3000 |

Your app just makes normal HTTP requests to http://localhost:3000/.... CORS is enabled out of the box, so cross-origin calls from any local dev server work with zero setup, and every request is logged to the mock's console. Nothing is imported into your app bundle — the interaction is pure HTTP.

Responses come from the three mock layers (headers, config routes, /api/mock); the degradation simulation, when enabled, layers realistic failure conditions on top of all of them.

What badmock simulates

The simulation is global and stateful — a condition the whole server is in, evolving over time and with traffic, not a per-route delay: 500. Four behaviors compose:

| Behavior | What it does | | --- | --- | | Load-adaptive latency | Latency rises with measured requests/sec over a rolling window (a server saturating) and falls when traffic eases. Linear or exponential curve. | | Bad periods | On a jittered schedule the server drops into a degraded window (~10s) where latency is amplified and requests may fail or hang, then it self-recovers. | | Hanging connections | Some requests hold the socket open with no response at all until the client aborts. This exercises your client's own timeout/AbortController handling — something a fixed delay can never do. | | Circuit breaker | After N consecutive failures the breaker flips open and rejects all traffic (503) for a cooldown, then half-opens to test recovery, then closes. |

A few deliberate design choices:

  • Injected failures don't feed the breaker. Only organic downstream 5xx (from your routes/mocks) count toward tripping it — so bad periods and the breaker never cascade into a stuck loop.
  • Half-open trials get a clean shot (load latency only, no injected hang/fail) so the recovery test is faithful.
  • maxHangMs: 0 means hang truly indefinitely (until the client gives up). Set it above 0 to fall back to a 504 after that long.
  • The reserved /api/* endpoints stay exempt, so you can always observe and reset the simulation even while everything else is failing.

Presets

npx badmock --simulate overloaded   # or: flaky, unstable

| Preset | Character | | --- | --- | | overloaded | A server buckling under load — latency climbs fast (exponential), with occasional bad periods and a forgiving breaker. | | flaky | An unreliable service — frequent failures during bad periods; the breaker trips readily and recovers quickly. | | unstable | Network instability — connections hang, bad periods bring more hangs and failures, moderate latency. |

Presets are a starting point. Override any individual field with a simulation block in the config file (file values win over the preset). The simulation layers on top of whatever mocks you define — fixed routes and header-shaped paths alike.

Observe and control the simulation

So you can see what the simulation is doing, and force states deterministically for demos and tests:

# live state
curl http://localhost:3000/api/sim/status
# → {
#     "enabled": true, "reqPerSec": 42, "mode": "bad_period",
#     "badPeriod": { "active": true, "endsInMs": 6200, "nextInMs": 0 },
#     "circuit": { "state": "open", "consecutiveFailures": 0, "openForMs": 11800 },
#     "addedLatencyMs": 2600, "hungRequests": 3
#   }

# force a bad period (optionally for N ms) or trip the breaker
# (Content-Type must be application/json so the body is parsed)
curl -X POST http://localhost:3000/api/sim/trigger \
  -H "Content-Type: application/json" -d '{"what":"bad_period","durationMs":10000}'
curl -X POST http://localhost:3000/api/sim/trigger \
  -H "Content-Type: application/json" -d '{"what":"circuit_open"}'

# clear everything (closes circuit, ends bad period, releases hung requests)
curl -X POST http://localhost:3000/api/sim/reset

Every response also carries observability headers you can read in the browser network tab:

  • X-Sim-Added-Latency — ms of latency the simulation added to a passed-through request.
  • X-Sim-Reason — on an injected response: circuit_open, bad_period, or hang.

You can drive all of this from code too — force a breaker open in a test setup file and assert your client falls back correctly. See the Programmatic API.

Every simulation field, type, and default is listed in the Simulation config reference.

Why not json-server or MSW?

Use whichever fits — they solve different problems:

| | Best at | Failure simulation | | --- | --- | --- | | json-server | Instant CRUD REST API from a JSON file, with persistence | — | | MSW | Intercepting requests inside your app/tests (no separate server) | Static (per-handler delay/error you code yourself) | | Mockoon | GUI-designed mock APIs | Static (per-route latency/rules) | | badmock | Stateful degradation + per-request response shaping | Dynamic — a live, evolving server condition: load-adaptive latency, hangs, bad periods, circuit breaker |

If you need a persistent fake database, use json-server. If you want request interception without a process, use MSW. If you want to know how your frontend behaves when the backend is having a bad day — that's badmock.

A complete mock API underneath

The simulation needs something to degrade — so badmock is also a full, zero-config mock server. Even with the chaos off, it covers everyday frontend work. Three ways to define what comes back, from most dynamic to most fixed; they compose freely:

  1. Header/query shaping — hit any path, control the response per-request: any status, delay, error, header.
  2. Config-file routes — a stable, repeatable fake API with :params.
  3. The explicit mock endpoint — describe the full response in one JSON body.

Shape any endpoint with headers

Hit any path with any method and shape the response with X-Mock-* request headers (recommended) or _-prefixed query params (handy for pasting a URL in the browser). Your client code looks exactly like it's calling a real API.

await fetch("http://localhost:3000/orders/42", {
  headers: { "X-Mock-Status": "500", "X-Mock-Delay": "1500" },
});
# query-param form — just paste in the browser address bar
http://localhost:3000/orders/42?_status=500&_delay=1500

Header and query reference

Headers win over query params when both are present.

| Header | Query param | Effect | | --- | --- | --- | | X-Mock-Status | _status | Force a status code (e.g. 404, 204, 418). | | X-Mock-Delay | _delay | Delay in ms. Also accepts a range like 200-1000 → random within it. | | X-Mock-Error | _error | Return an error body of a named type (below). Sets the matching status unless X-Mock-Status overrides it. | | X-Mock-Fail | _fail | Probability 0..1 of randomly turning a success into an error. | | X-Mock-Header-* | — | Set an arbitrary response header, e.g. X-Mock-Header-X-RateLimit: 99 → response gets X-RateLimit: 99. |

Named error types (X-Mock-Error / _error): unauthorized (401), forbidden (403), not_found (404), conflict (409), bad_request (400), custom_error (500).

Response body rules

  • Success — echoes the JSON body you sent (great for POST/PUT). With no body you get { "ok": true }. POST defaults to 201, everything else to 200. Status 204 returns no content.
  • Error{ "error": "not found", "code": "NOT_FOUND" }.

Define a fixed API with a config file

When you want a stable fake API — real paths that always return the same data — drop a badmock.config.json next to where you run the command. It's auto-detected (or pass --config <file>).

{
  // Raw value → returned as the response body with status 200
  "GET /users": [{ "id": 1, "name": "Ada" }],

  // ":id" params work — real path routing
  "GET /users/:id": { "id": 1, "name": "Ada" },

  // Full spec object → control status / body / delay / headers / error
  "POST /login": { "status": 401, "body": { "message": "bad credentials" } },
  "GET /slow":    { "delay": "500-1500", "body": { "ok": true } },
  "GET /missing": { "error": "not_found" }
}

Two behaviors worth knowing:

  • X-Mock-* headers still override a configured route — e.g. force /users to 500 to test error handling without editing the file.
  • Any path not in the config falls back to header/query shaping.

Route keys, every spec-object field, and the structured form (separate routes + simulation keys) are documented field-by-field in the Configuration reference. See badmock.config.example.json for a complete example.

The explicit mock endpoint

If you'd rather describe the entire response in a single JSON body instead of headers, POST to /api/mock. The body is a discriminated union on type.

Shared fields (both branches):

{
  delay?: number;            // ms (0..300000)
  randomDelay?: boolean;     // enable a random delay window
  randomDelayMinMs?: number; // default 0
  randomDelayMaxMs?: number; // default 2000
  headers?: Record<string, string>;
  allow_fail?: boolean;      // if true, outcome may randomly flip
  fail_probability?: number; // 0..1
}

Success (type: "success") — responds { "ok": true, "data": <mockData> }:

{ type: "success", mockData?: unknown, status?: number /* 2xx */ }

Error (type: "error") — responds { "error", "code", "details" }:

{
  type: "error",
  errorType: "unauthorized" | "forbidden" | "not_found" | "conflict" | "bad_request" | "custom_error",
  status?: number, // 4xx/5xx override
  error?: { message?: string, code?: string, details?: unknown }
}

Notes: delay takes precedence over randomDelay; when allow_fail + fail_probability are set, the server picks the outcome randomly, then uses your success or error fields accordingly.

curl -X POST http://localhost:3000/api/mock \
  -H "Content-Type: application/json" \
  -d '{ "type": "error", "errorType": "not_found", "delay": 800 }'

Recipes

Client timeout — make it hang and check your AbortController fires:

npx badmock --simulate unstable
fetch("http://localhost:3000/report"); // may hang until your client aborts

Resilience demo — run --simulate overloaded, hammer the API, and watch X-Sim-Added-Latency climb via /api/sim/status.

Breaker fallback in a test — trip the breaker deterministically, assert your UI shows its degraded state, then reset (or use the programmatic API).

Loading spinner — add latency:

fetch("http://localhost:3000/dashboard", { headers: { "X-Mock-Delay": "2000" } });

Auth redirect — force a 401:

fetch("http://localhost:3000/me", { headers: { "X-Mock-Error": "unauthorized" } });

Retry logic — 30% random failures:

fetch("http://localhost:3000/flaky", { headers: { "X-Mock-Fail": "0.3" } });

Rate-limit UI — inject a header:

fetch("http://localhost:3000/search", {
  headers: { "X-Mock-Status": "429", "X-Mock-Header-Retry-After": "30" },
});

Installation

Requires Node 18+. Three ways to run it:

1. Instantly with npx (no install):

npx badmock

2. As a project dev-dependency with an npm script:

npm install -D badmock
// package.json
{
  "scripts": {
    "mock": "badmock --port 3000"
  }
}
npm run mock

3. Globally, if you use it across projects:

npm install -g badmock
badmock

Note: npm install only copies files — you start the server by running the badmock command. See the FAQ.

Configuration reference

Everything badmock reads at startup, in one place: where configuration comes from, the config-file shapes, every route field, the simulation block, and how sources layer.

Where configuration comes from

| Source | How | Notes | | --- | --- | --- | | Config file | badmock.config.json, auto-detected in the current working directory | Override the path with --config <file>; ignore an auto-detected file with --no-config. | | CLI flags | See the CLI reference | Flags override the file. | | Environment variables | PORT, HOST | Used as the default port/host when --port / --host aren't passed. |

The config file has two shapes:

// Flat — the whole object is a route map
{ "GET /users": [{ "id": 1 }] }
// Structured — separate routes and simulation ("routes" and "simulation" are reserved keys)
{
  "routes":     { "GET /users": [{ "id": 1 }] },
  "simulation": { "load": { "maxMs": 3000 } }
}

Route definitions

Each entry maps a route key to a response.

Route key"METHOD /path":

  • The method is optional — omit it ("/users") to match any method.
  • Paths support :params, e.g. "GET /users/:id".

Response value — either raw data or a spec object:

  • Raw data (array, object, string, number) → returned verbatim as the body, status 200 (201 for POST).
  • Spec object → any combination of these fields:

| Field | Type | Description | | --- | --- | --- | | status | number | Response status code. | | body | any JSON | Response body. Wrap raw data here if it itself has a reserved key (status, delay, etc.). | | delay | number | string | Delay in ms, or a "min-max" range string (e.g. "500-1500") → random within it. | | headers | object | Response headers to set. | | error | string | Named error type (unauthorized, forbidden, not_found, conflict, bad_request, custom_error); sets the matching status and a { error, code } body unless status/body override it. |

If neither status nor error is set, success defaults apply (201 for POST, else 200). X-Mock-* request headers still override a configured route at request time.

{
  "GET /users":     [{ "id": 1, "name": "Ada" }],          // raw data
  "GET /users/:id": { "id": 1, "name": "Ada" },            // :param
  "POST /login":    { "status": 401, "body": { "message": "bad credentials" } },
  "GET /slow":      { "delay": "500-1500", "body": { "ok": true } },
  "GET /missing":   { "error": "not_found" },
  "DELETE /orders/:id": { "status": 204 }                  // any-method form also allowed: "/orders/:id"
}

Simulation config reference

Add a simulation object (structured form) to enable the degradation simulation from the config file. All fields are optional; anything omitted falls back to the default below (or the preset, if one is active — file values win over the preset).

{
  "simulation": {
    "enabled": true,

    "load": {
      "baselineMs": 20,        // latency floor added to every request
      "windowMs": 1000,        // rolling window used to measure req/sec
      "ratePerSecForMax": 40,  // req/sec at which added latency hits maxMs
      "maxMs": 3000,           // max added latency
      "curve": "linear"        // "linear" | "exponential"
    },

    "badPeriod": {
      "enabled": true,
      "everyMs": 60000,        // roughly how often one starts (jittered 0.5x–1.5x)
      "durationMs": 10000,     // how long it lasts
      "latencyMultiplier": 5,  // latency ×5 while active
      "failProbability": 0.4,  // chance a request fails (503) while active
      "hangProbability": 0.1   // chance a request hangs while active
    },

    "hang": {
      "probability": 0,        // baseline chance any request hangs
      "maxHangMs": 0           // 0 = indefinite; >0 = fall back to 504 after this
    },

    "circuitBreaker": {
      "enabled": true,
      "failureThreshold": 5,       // consecutive organic 5xx to open
      "openMs": 15000,             // cooldown before half-open
      "halfOpenTrialRequests": 1,  // requests allowed through to test recovery
      "rejectStatus": 503          // status returned while open
    }
  }
}

Precedence

  • Routes: startServer({ routes }) option → config file.
  • Simulation: preset (--simulate) → config-file simulation block → explicit startServer({ simulation }) (later wins).
  • Per request: X-Mock-* headers / _query params override a matching config route.

CLI reference

badmock [options]

| Flag | Description | | --- | --- | | -s, --simulate [preset] | Enable degradation simulation. Preset is overloaded (default), flaky, or unstable. | | --no-simulate | Disable simulation even if the config enables it. | | -p, --port <n> | Port to listen on. Default 3000 (or $PORT). | | --host <h> | Host/interface to bind. Default 127.0.0.1 (or $HOST). | | -c, --config <file> | Load routes/simulation from a JSON file. Auto-detects ./badmock.config.json. | | --no-config | Ignore an auto-detected config file. | | -v, --version | Print the version. | | -h, --help | Show help. |

Stop the server with Ctrl+C.

Built-in endpoints

Reserved /api paths sit alongside the catch-all mock and are exempt from the simulation:

| Method | Path | Description | | ---: | --- | --- | | GET | /api/sim/status | Live simulation state. | | POST | /api/sim/trigger | Force a bad period or trip the breaker. | | POST | /api/sim/reset | Clear all simulation state and release hung requests. | | POST | /api/mock | Explicit response simulator (full spec in the JSON body). | | GET | /api/status | Health check → { "status": "OK" }. |

Programmatic API

You can also drive the mock from code (e.g. a test setup file). The package exports:

import {
  startServer,      // boot the server; returns an http.Server with `.simEngine`
  createApp,        // build the app (no listen)
  loadConfig,       // read + parse a config file
  normalizeConfig,  // split a loaded config into { routes, simulation }
  findConfig,       // auto-detect a config path in a directory
  PRESETS,          // the built-in simulation presets
  DegradationEngine,
} from "badmock";

startServer(options):

{
  port?: number;               // default $PORT or 3000
  host?: string;               // default $HOST or "127.0.0.1"
  silent?: boolean;            // suppress the "listening" log line
  log?: boolean;               // per-request logging (default true unless silent)
  routes?: RouteConfig;        // fixed routes, already parsed
  configPath?: string;         // load routes + simulation from a file
  simulation?: SimConfigInput; // explicit simulation overrides
  simulatePreset?: string;     // "overloaded" | "flaky" | "unstable"
}

Example — start a mock around a test run and drive the simulation directly:

import { startServer } from "badmock";

const server = startServer({ port: 4000, silent: true, simulatePreset: "flaky" });

// force states deterministically in a test
server.simEngine?.trigger("circuit_open");
// ... assert your client falls back correctly ...
server.simEngine?.reset();

server.close(); // stops the server and the engine's timers

FAQ

Does npm install start the server? No. Installing only copies files into node_modules. Start it by running the badmock command (npx badmock, an npm script, or node). A package that spawned a server on install would be surprising and unsafe.

Where does the config file live? In your project — in the directory where you run the command (typically next to your package.json), not inside the badmock package.

Is the config hot-reloaded? Not yet — it's read once at startup. Edit it and restart to pick up changes. Hot reload is on the roadmap.

Hanging vs. a big delay — what's the difference? A delay responds eventually (after N ms). A hang never responds; the socket stays open until the client aborts. Only a hang exercises your client's timeout/AbortController path.

Does it call or test my real API? No, never. badmock is a fake API that you point your frontend at; it makes no outbound requests.

Any dependencies? None. It's built on Node's built-in http module and installs in ~100 kB.

Roadmap

  • Config hot-reload — pick up badmock.config.json changes without a restart.
  • Scenario scripting — a timeline of simulation states ("2 min healthy, then degrade, then outage") for demos and E2E runs.
  • Request assertions — optionally record received requests so tests can assert what the frontend sent.

Have a use case? Open an issue — see CONTRIBUTING.md.

Local development

npm install
npm run dev     # hot-reload dev server (ts-node-dev on src/server.ts)
npm run build   # compile TypeScript to dist/
npm test        # build + run the node:test suite against dist/
npm start       # run the built CLI (node bin/cli.js)

Tests use Node's built-in test runner — no framework, keeping the package at zero dependencies. They run against the compiled dist/, i.e. exactly what ships to npm.

The published package ships only the compiled dist/, the CLI in bin/, the example config, this README, and the license.

License

MIT — see LICENSE.