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

@zethictech/mockr

v0.1.0

Published

Lightweight local mock API server with a web UI, JavaScript handlers and hot reload

Readme

mockr

npm node license

A local mock API server you configure in a browser and extend with plain JavaScript. No database, no account, no cloud.

cd your-project
npx @zethictech/mockr

That scaffolds a project, starts a mock server on :4000 and opens a UI on :4100. Anything you save in the UI is live on the next request.

  mockr  3 route(s)

  ● mock   http://127.0.0.1:4000
  ● ui     http://127.0.0.1:4100

Why

You need an API that does not exist yet, or one that exists but will not return the failure you are trying to handle. Mockr gives you both:

  • Static routes for the ordinary case — a method, a path, a JSON body.
  • JavaScript handlers when the response has to be computed, and interceptors when the request or response has to be transformed — decryption, encryption, validation, simulated auth.

Everything lives in files in your repo, so it is reviewable and committable like the rest of your project.


Using it in your project

1. Start it

Add it to your dev scripts so it comes up with everything else:

{
  "devDependencies": { "@zethictech/mockr": "^0.1.0" },
  "scripts": {
    "mock": "mockr",
    "dev": "npm run mock & vite"
  }
}

Or run it on demand with npx @zethictech/mockr. No install required.

The package is @zethictech/mockr; the command it installs is mockr.

2. Point your app at it

- VITE_API_URL=https://api.example.com
+ VITE_API_URL=http://localhost:4000

CORS is permissive by default and preflight is answered automatically, so a browser app on any port can call it without configuration.

3. Create routes

Open http://localhost:4100, click New route, fill in a method, a path and a body, and save. The endpoint answers immediately — no restart, no build.

Everything the UI does is written to mockr.json in your project, so you can also skip the UI entirely and edit that file by hand. Both directions hot reload, and the UI notices external edits within two seconds.

4. Commit it

mockr.json          the routes
handlers/           JavaScript route handlers
interceptors/       request and response transforms

Commit all three and your team gets the same mocks.


Routes

Static

The common case. Configure it in the UI, or write it directly:

{
  "method": "GET",
  "path": "/users",
  "response": {
    "status": 200,
    "body": { "users": [] }
  }
}

Path parameters

{
  "method": "GET",
  "path": "/users/:id",
  "response": { "status": 200, "body": { "id": 1 } }
}

Static segments win over parameters, so /users/me beats /users/:id regardless of the order they appear in.

Slow and failing responses

{
  "method": "POST",
  "path": "/checkout",
  "response": {
    "status": 503,
    "delayMs": 3000,
    "body": { "error": "service unavailable" }
  }
}

This is the reason most people reach for a mock server: reproducing the timeout or the 500 that is awkward to trigger against a real backend.

Custom headers

{
  "method": "GET",
  "path": "/download",
  "response": {
    "status": 200,
    "headers": { "content-type": "text/csv" },
    "body": "id,name\n1,Ada"
  }
}

Handlers

Write these in the UI's Code tab, or in your own editor — both hot reload, and the UI picks up external edits within two seconds.

When the response depends on the request, point the route at a file instead of a body:

{ "method": "POST", "path": "/login", "handler": "login" }
// handlers/login.js
module.exports = async function (ctx) {
  const { email, password } = ctx.request.body;

  if (password !== 'hunter2') {
    const err = new Error('invalid credentials');
    err.status = 401;
    err.body = { error: 'invalid credentials' };
    throw err;
  }

  return {
    status: 200,
    body: { token: 'abc123', email },
  };
};

A route has either a response or a handler, never both.

In the UI, choosing JavaScript handler on a route gives you a dropdown of what exists and an Edit code → link into the editor. New files start from a template. Code that does not parse is refused on save, so a typo can never replace a handler that was working.

Using npm packages

Handlers and interceptors are ordinary Node modules, so they can require anything installed in your own project. Install it the normal way:

npm install jsonwebtoken

Then require it. Nothing to register, no config:

// interceptors/requireAuth.js
const jwt = require('jsonwebtoken');

const SECRET = process.env.MOCK_JWT_SECRET || 'dev-secret';

module.exports = async function (ctx) {
  const header = ctx.request.headers.authorization || '';
  const token = header.startsWith('Bearer ') ? header.slice(7) : null;

  if (!token) {
    ctx.response = { status: 401, headers: {}, body: { error: 'missing bearer token' } };
    return;
  }

  try {
    // Anything you attach to ctx is visible to the handler that runs next.
    ctx.request.user = jwt.verify(token, SECRET);
  } catch (err) {
    ctx.response = { status: 401, headers: {}, body: { error: err.message } };
  }
};

Attach it to any route that should require a token:

{
  "method": "GET",
  "path": "/me",
  "handler": "me",
  "request": { "interceptors": ["requireAuth"] }
}

And the handler reads what the interceptor left behind:

// handlers/me.js
module.exports = async function (ctx) {
  return { body: { id: ctx.request.user.sub, email: ctx.request.user.email } };
};

Environment variables work as usual, so secrets stay out of the repo:

MOCK_JWT_SECRET=something-else npx @zethictech/mockr

If you forget to install the package, the route returns an error naming the missing module and the UI shows it. Installing it fixes things on the next request — no restart, and no need to touch the file, because a failed load is never cached.

Mockr installs nothing on your behalf and bundles nothing.

What a handler receives

ctx.request.method; // "POST"
ctx.request.path; // "/users/42"
ctx.request.params; // { id: "42" }        from /users/:id
ctx.request.query; // { page: "2" }       from ?page=2
ctx.request.headers; // lowercased keys
ctx.request.body; // parsed by content-type
ctx.route.id; // the route that matched

What a handler returns

return { status: 200, headers: {}, body: {} };

All three are optional. status defaults to 200, and returning nothing at all produces a 204.

To fail with a specific status, throw an error carrying one:

const err = new Error('not found');
err.status = 404;
err.body = { error: 'not found' };
throw err;

An unexpected throw becomes a 500 naming the handler, and the stack is printed to the terminal.


Built-in auth

Requiring a valid token is the most common reason to write an interceptor, and it is the same interceptor every time. Mockr ships it. Built-ins start with @, so they never collide with your own files, and they are configured rather than written:

{
  "interceptors": {
    "@jwt": { "secret": "${MOCK_JWT_SECRET}" },
    "@apiKey": { "key": "sk_test_123" }
  },

  "handlers": {
    "@jwt.sign": { "secret": "${MOCK_JWT_SECRET}", "expiresInSeconds": 3600 }
  },

  "routes": [
    { "method": "POST", "path": "/login", "handler": "@jwt.sign" },

    { "method": "GET", "path": "/me", "handler": "whoami", "request": { "interceptors": ["@jwt"] } },

    {
      "method": "GET",
      "path": "/paid",
      "response": { "status": 200, "body": { "ok": true } },
      "request": { "interceptors": ["@apiKey"] }
    }
  ]
}
MOCK_JWT_SECRET=dev-secret npx @zethictech/mockr

${VAR} is expanded from the environment, so a secret is named in the file rather than written into it — mockr.json gets committed with your project.

Now the whole flow works:

# get a token
curl -X POST localhost:4000/login -H 'content-type: application/json'   -d '{"sub":"42","email":"[email protected]"}'
# → { "token": "eyJhbGci…", "expiresIn": 3600 }

curl localhost:4000/me                                  # → 401 missing authorization header
curl localhost:4000/me -H "authorization: Bearer $TOKEN" # → { "sub": "42" }

@jwt puts the decoded claims on ctx.request.user, so your handler just reads them:

module.exports = (ctx) => ({ body: { sub: ctx.request.user.sub } });

| Built-in | | | ----------- | ----------------------------------------------------------- | | @jwt | Verify a bearer token, attach claims to ctx.request.user | | @apiKey | Require a matching key in a header or query parameter | | @jwt.sign | Issue a token from the request body (a mock login endpoint) |

@jwt options

| | default | | | --------------------- | --------------- | -------------------------------------------- | | secret | required | HMAC secret | | algorithms | ["HS256"] | allowed algorithms — HS256, HS384, HS512 | | header | authorization | where to read the token | | scheme | Bearer | prefix to strip; "" for a bare token | | attachTo | user | property on ctx.request to hold the claims | | optional | false | allow anonymous requests through | | issuer / audience | | checked when set | | clockTolerance | 0 | seconds of leeway on exp and nbf |

Verification is HMAC only, done with node:crypto rather than a JWT dependency. Unsigned tokens (alg: none) and algorithms outside the allowed list are rejected.

Interceptors

Interceptors are written the same way — the Code tab, or your editor — and are attached per route, so one implementation can serve many endpoints:

{
  "method": "POST",
  "path": "/payment",
  "request": { "interceptors": ["decrypt", "validate"] },
  "response": {
    "status": 200,
    "body": { "ok": true },
    "interceptors": ["encrypt"]
  }
}

Interceptors mutate ctx. Return values are ignored.

They also receive their own settings from mockr.json, so nothing has to be hardcoded:

{
  "interceptors": {
    "decrypt": { "algorithm": "aes-256-gcm", "key": "${PAYLOAD_KEY}" }
  }
}
module.exports = async function (ctx, config) {
  ctx.request.body = decrypt(ctx.request.body, config.key, config.algorithm);
};

Handlers get the same, from a handlers block.

// interceptors/decrypt.js
module.exports = async function (ctx) {
  ctx.request.body = JSON.parse(decrypt(ctx.request.body.payload));
};
// interceptors/encrypt.js
module.exports = async function (ctx) {
  ctx.response.body = { payload: encrypt(JSON.stringify(ctx.response.body)) };
};

ctx.response exists only in the response phase. During the request phase it is undefined, because the response has not been produced yet.

Simulating auth

Setting ctx.response during the request phase ends the request early — the handler and any remaining request interceptors are skipped:

// interceptors/requireAuth.js
module.exports = async function (ctx) {
  if (!ctx.request.headers.authorization) {
    ctx.response = { status: 401, headers: {}, body: { error: 'unauthorized' } };
  }
};

Validating input

// interceptors/validate.js
module.exports = async function (ctx) {
  if (!ctx.request.body.email) throw new Error('email is required');
};

A throw in a request interceptor is a 400 carrying your message. A throw in a response interceptor is a 500, because by then the request was already accepted and the failure is the server's.


The order things run

request → route lookup → request interceptors → handler or static response
        → response interceptors → delay → response

Hot reload

Editing mockr.json, any handler, or any interceptor takes effect on the next request. The server is never restarted.

If you save something broken — invalid JSON, a syntax error in a handler — Mockr keeps serving the last working configuration and shows the error in the UI and the terminal. It does not crash and it does not start returning 404s for routes you did not touch.


CommonJS, and ESM projects

Handlers and interceptors are CommonJS (module.exports). This is not stylistic: hot reload needs to drop modules from Node's require cache, and the ESM loader has no equivalent. Supporting import would mean losing reload, which is the feature you would be using them for.

If your project has "type": "module" in package.json, plain .js files are ESM and cannot use module.exports. Mockr detects this and scaffolds .cjs instead:

handlers/login.cjs
interceptors/requireAuth.cjs

Both extensions work everywhere, and routes still reference them without one ("handler": "login"). If you hand-write a .js file in an ESM project, the error tells you exactly which file to rename.


Ports and settings

Both ports are configurable, either per run or per project.

On the command line

npx @zethictech/mockr --port 4000 --admin-port 4100

In mockr.json

So the whole team gets the same ports without typing flags:

{
  "server": {
    "port": 4000,
    "adminPort": 4100,
    "host": "127.0.0.1",
    "cors": true,
    "quiet": false
  },
  "routes": []
}

Flags beat the file, and the file beats the defaults — per setting. So this keeps the file's adminPort and overrides only the mock port:

npx @zethictech/mockr --port 6001
  ● mock   http://127.0.0.1:6001  (flag)
  ● ui     http://127.0.0.1:5556  (file)

Saving routes from the UI never touches your server block.

port, adminPort and host are bound once at startup. Editing them while Mockr is running prints a warning telling you to restart, rather than letting the change look like it applied. Everything else hot reloads.

CLI

mockr                    # start, scaffolding first if needed
mockr init               # scaffold only

| Flag | Default | | | ---------------------- | ----------- | ----------------------- | | -p, --port | 4000 | mock server port | | --admin-port | 4100 | UI and API port | | --host | 127.0.0.1 | bind address | | -d, --dir | cwd | project directory | | --cors / --no-cors | on | permissive CORS | | -q, --quiet | | silence the request log |


Configuration reference

{
  method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD'
  path: string                          // "/users/:id"

  request?: {
    interceptors?: string[]
  }

  // exactly one of response / handler
  response?: {
    status?: number                     // default 200
    delayMs?: number                    // default 0
    headers?: Record<string, string>
    body?: unknown                      // omit for 204
    interceptors?: string[]
  }
  handler?: string                      // "login" → handlers/login.js
}

id is added and managed by Mockr. You never need to write one.


Management API

The UI is a client of this, and so can your scripts be. It runs on the admin port.

GET    /api/routes
GET    /api/routes/:id
POST   /api/routes
PUT    /api/routes/:id
DELETE /api/routes/:id

GET    /api/handlers          names available to route at
GET    /api/interceptors
GET    /api/status            route count, load errors

GET    /api/handlers/:name    read source
PUT    /api/handlers/:name    write source  { "source": "..." }
DELETE /api/handlers/:name
GET    /api/interceptors/:name
PUT    /api/interceptors/:name
DELETE /api/interceptors/:name

Module writes are parsed before they are saved and rejected with 422 if they do not compile, so the running server keeps the last working version.

Writes are validated before they touch the file, and rejected ones come back as 422 listing every problem at once.


Two ports, on purpose

The mock server and the admin API are separate so that /api/* and / stay yours to mock. With a single port those paths would be permanently reserved by Mockr itself.


Security

Mockr runs your handlers and interceptors as ordinary Node code, in process, with no sandbox. The management API is unauthenticated. Both servers therefore bind to 127.0.0.1.

--host 0.0.0.0 exposes arbitrary local code execution to your network. Only do it on a network you trust.


What it deliberately does not do

No regex or wildcard routes, request recording, response scenarios, OpenAPI import, GraphQL, WebSockets, auth, multi-user, or a database.

It does not manage your dependencies. Handlers can require anything in your project's node_modules, but installing them is yours to do.

There is also no passthrough proxy: a request that matches no route is a 404, not a forwarded call to a real backend. If only part of your API is mocked, point the mocked calls at Mockr and leave the rest alone, or route them with your dev server's own proxy rules.


Development

npm install
npm run dev

That runs the server against a scratch project in demo/, with the UI rebuilding on save:

  mock server   http://localhost:4000
  admin + UI    http://localhost:4100

Two ports, the same two the product uses — the UI is served by the admin server, in development exactly as in production. Editing anything under ui/ rebuilds in about 200ms; refresh to see it.

| | | | ------------------- | ------------------------------------------- | | npm run dev | server plus UI rebuild, against demo/ | | npm run build | server to dist/, UI to dist/ui/ | | npm test | unit and end-to-end tests, including reload | | npm run typecheck | both TypeScript projects |

demo/ is gitignored and scaffolds itself on first run.

The architecture and the reasoning behind each decision are in docs/spec.md.

Contributing

Contributions are welcome — bug fixes, tests, documentation and platform fixes especially. CONTRIBUTING.md covers the setup, how the code is laid out, and the few places where a change is most likely to break something quietly.

Larger changes are worth an issue first, since Mockr keeps its scope deliberately narrow.

License

MIT