Start Your Project Today
Tell us about your project — we’ll get back within 24 hours
Founder
Mock API server npm
Mock REST API locally
Mock server with hot reload
Self-hosted API mocking tool
npx @zethictech/mockr
This post explains the problem that produced it, because the problem is more common than the tool. If you have ever needed to build a frontend against an API you could not actually call, the rest of this will be familiar.

Bandhan Bank is one of India’s larger commercial banks, serving over 3 crore customers through a workforce of more than 75 thousand people. We had built for them before, a banking operations management system that replaced a cluttered core banking interface. That work sits inside Zethic’s broader banking solutions practice, where isolated, regulation-bound environments are the norm rather than the exception.
For the next project, the bank’s APIs lived inside the bank, and they were staying there. Not behind a login we could be issued. Not behind a rate limit we could negotiate. Inside a strictly isolated environment, built on the assumption that anything outside it is hostile until proven otherwise, and offering no mechanism whatsoever for proving otherwise from a developer’s desk. We asked all the obvious questions and got the same answer to every one of them:
How do you run a local mock API server?cd your-project
npx @zethictech/mockr
That scaffolds a project, starts a mock server on port 4000 and opens a browser UI on port 4100:
mockr 3 route(s)
● mock http://127.0.0.1:4000
● ui http://127.0.0.1:4100
Point your app at it by changing one environment variable:
- 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 works without extra configuration.
To bring it up with the rest of your dev environment, including inside a CI/CD pipeline, add it to your scripts:
{
"devDependencies": { "@zethictech/mockr": "^0.1.0" },
"scripts": {
"mock": "mockr",
"dev": "npm run mock & vite"
}
}
http://localhost:4100, click New route, and fill in a method, a path and a body. The endpoint answers on the next request with no restart and no rebuild, which is what it means to mock REST API locally instead of against a shared, remote stub.
Everything the UI does is written to mockr.json in your project, so you can skip the UI entirely and edit the file directly:
{
"method": "GET",
"path": "/users/:id",
"response": {
"status": 200,
"body": { "id": 1, "name": "Ada" }
}
}
Both directions hot reload, so this behaves as a mock server with hot reload built in: no restart, no rebuild. Static path segments beat parameters, so /users/me wins over /users/:id regardless of the order they appear in.
{
"method": "POST",
"path": "/checkout",
"response": {
"status": 503,
"delayMs": 3000,
"body": { "error": "service unavailable" }
}
}
Three seconds of silence, then a 503. Against a real backend, reproducing that on demand ranges from awkward to impossible. Here it takes ten seconds and a saved file, and your loading state, retry logic and error boundary all get tested by someone other than a customer, which is particularly important for transaction-heavy systems such as crypto exchange software
{ "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 handler receives the parsed request and returns a response:
On ctx.request |
Contains |
|---|---|
| method | “POST” |
| path | “/users/42” |
| params | { id: "42" } from /users/:id |
| query | { page: "2" } from ?page=2 |
| headers | lowercased keys |
| body | parsed by content type |
require anything installed in your own project. Mockr installs nothing on your behalf and bundles nothing.
{
"method": "POST",
"path": "/payment",
"request": { "interceptors": ["decrypt", "validate"] },
"response": {
"status": 200,
"body": { "ok": true },
"interceptors": ["encrypt"]
}
}
// interceptors/decrypt.js
module.exports = async function (ctx, config) {
ctx.request.body = decrypt(ctx.request.body.payload, config.key, config.algorithm);
}
Interceptors receive their settings from mockr.json, and ${VAR} is expanded from the environment, so a key is named in the committed file rather than written into it:
{
"interceptors": {
"decrypt": { "algorithm": "aes-256-gcm", "key": "${PAYLOAD_KEY}" }
}
}
That is the piece that made Mockr viable rather than merely convenient. Our client code decrypted a real payload encrypted with the real algorithm. When we finally pointed the app at the bank’s actual environment, the cryptography was not the thing that broke.
@ and are configured rather than written:
{
"interceptors": { "@jwt": { "secret": "${MOCK_JWT_SECRET}" } },
"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"] } }
]
}
| Built-in | What it does |
|---|---|
| @jwt | Verifies a bearer token and attaches claims to ctx.request.user |
| @apiKey | Requires a matching key in a header or query parameter |
| @jwt.sign | Issues a token from the request body, giving you a mock login endpoint |
node:crypto rather than a JWT dependency. Unsigned tokens (alg: none) and algorithms outside the allowed list are rejected.
mockr.json the routes
handlers/ JavaScript route handlers
interceptors/ request and response transforms
You commit all three, and that single decision fixes the problem the hand rolled servers created. A mock stops being a private artifact on one laptop and becomes a reviewable change. When a contract changes, one engineer updates the mock and opens a pull request. Reviewers see the new response shape in the diff. Everyone else gets it on git pull.
The mock stops being folklore and starts being code.
| Runs locally | Config in a browser UI | Custom JS logic | Encrypted payload support | Cloud account needed | |
|---|---|---|---|---|---|
| Mockr | Yes | Yes | Handlers and interceptors | Yes, via interceptors | No |
| json-server | Yes | No | Limited | No | No |
| Mockoon | Yes | Yes (desktop app) | Yes | Partial | No |
| WireMock | Yes | Partial | Yes (Java) | Yes | No |
| Hosted mock services | No | Yes | Varies | Varies | Yes |
| Mock Service Worker | In the client | No | Yes | Yes | No |
127.0.0.1 by default. Running with --host 0.0.0.0 exposes arbitrary local code execution to your network, so only do it on a network you trust.
npx @zethictech/mockr
Let Zethic help you build smarter Not just faster
Yes. Commit mockr.json, handlers/ and interceptors/, and every teammate gets the same mocks on git pull.

Builds backend systems, AI products, and open-source tools at Zethic. Enjoys taking complex, messy problems and figuring out the architecture, implementation, and everything in between to get them running reliably in production.
Adding {{itemName}} to cart
Added {{itemName}} to cart