We built and open sourced Mockr, an open source mock API server you configure in a browser and extend with plain JavaScript. It runs on your machine, stores nothing in the cloud, and starts with one command:
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.
What is a mock API server?
A mock API server is a program that stands in for a real backend during development. It listens on a local port, matches incoming requests against routes you have defined, and returns responses you control.
Developers reach for one in three situations:
- The API does not exist yet. The contract is agreed but the implementation is weeks away.
- The API exists but cannot be reached. It sits inside a network your machine is not part of.
- The API exists and works too well. You need it to return a 500 or hang for thirty seconds, and it stubbornly refuses to do either on demand.
We hit all three on the same project.
Why was the banking environment so difficult to reach?
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:
- No VPN tunnel for the development team.
- No IP whitelisting for our office.
- No temporary credentials, no jump host, no bastion.
- No read-only window into a sandbox copy.
Not a slow yes, and not a yes with conditions attached. The environment was isolated by design, and the design did not include a door for vendors to be let through.
This was not a ticket waiting on an approval. For an institution holding the deposits of 3 crore customers, an environment that cannot be reached from the outside is not excessive caution. It is the control itself. Punch one hole in it for a development team and the property that made it worth building is gone.
And none of it changed the delivery date. The bank was right to keep its environment sealed and also expected a working product on the agreed timeline. Both positions were reasonable at the same time, which meant nobody was going to resolve the tension for us.
Why didn’t the API return plain JSON?
This is the requirement that rules out most API mocking tools, and it is worth stating plainly because so few setups handle it.
The bank’s endpoints did not exchange readable JSON. A request left as an encrypted envelope and a response came back as one. The first thing our client did with any response was decrypt it. The last thing it did with any request was encrypt it. On most screens, that path was not a detail at the edge of the integration. It was the integration.
A mock that hands back clean, readable JSON exercises everything in the client except the part most likely to break. You can build a feature against a mock like that, watch it work perfectly for six weeks, and discover on the day access finally arrives that none of it was ever tested end to end, because the code that mattered was never asked to run.
So whatever we used had to do real cryptographic work: decrypt on the way in, encrypt on the way out, with the real algorithm, against a key supplied by the environment rather than committed to the repository.
What did we try before building Mockr, and why didn’t hardcoded JSON fixtures work?
The fastest option, and the first to rot. Fixtures drift out of sync with the contract, need a rebuild to change, and have a habit of reaching a production branch. Critically, a fixture can only describe success. You cannot hardcode a timeout.
Why could we not use a cloud mock API service?
Cloud mocking services are good products, and for many teams they are the right answer. They were ruled out here in one sentence.
Configuring a hosted mock means uploading the request and response shapes of the API you are mocking. If that API belongs to a bank’s internal systems, those shapes are not yours to upload. In a regulated environment the first question is never “is this vendor trustworthy.” It is “where does this data sit,” and “a third party’s cloud” ends the discussion.
This is the single most common reason teams in banking, healthcare, insurance and government search for a self hosted API mocking tool rather than a SaaS one.
Why did a hand rolled Express mock server fall short?
This is what competent teams actually do, and it works for about two weeks. Then the failure mode arrives, and it is social rather than technical.
Everyone wrote their own. One developer’s mock returned a field another developer’s did not, so bugs reproduced on one machine and not the next. The mock servers lived in nobody’s repository, so they were never reviewed and never shared. When the bank revised a contract, the update propagated by word of mouth, which means it reached about half the team.
The mocks became a second, undocumented, contradictory source of truth about the API. That is worse than having no mocks, because now people trust them.
None of the three handled the encryption requirement well. Fixtures cannot encrypt at all. The hand rolled servers could, and that was its own problem: four people reimplementing the same crypto path without comparing notes.
How do you run a local mock API server?
Mockr needs Node 20 or newer and no installation:
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"
}
}
How do you create mock REST API routes?
Open 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.
How do you simulate slow API responses and server errors?
This is the reason most teams reach for a mock server, and it takes three lines:
{
"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.
How do you write a JavaScript mock API handler?
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 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 |
Handlers are ordinary Node modules, so they can require anything installed in your own project. Mockr installs nothing on your behalf and bundles nothing.
How do you mock an API that uses encrypted payloads?
This is what interceptors are for. They sit either side of the handler and transform the request on the way in or the response on the way out:
{
"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.
How do you mock JWT authentication?
Requiring a valid token is the most common interceptor anyone writes, and it is the same one every time, so Mockr ships it. Built-ins start with @ 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 |
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.
Why should you commit your mocks to version control?
Everything Mockr knows lives in three places in your project:
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.
How does Mockr compare with other API mocking tools?
As an open source mock API server, Mockr sits alongside several other tools worth knowing before you choose one.
| | 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 |
These tools solve overlapping problems and several are more capable than Mockr in areas it deliberately avoids. Check each project’s current documentation before choosing. Mockr’s narrow bet is the combination of a browser UI, committed config, and interceptors that can do real cryptographic work, on a machine that never talks to anything.
What does Mockr deliberately not do?
A tool’s boundaries are as informative as its features:
- No regex or wildcard routes
- No request recording or response scenarios
- No OpenAPI import, GraphQL or WebSockets
- No multi-user mode, database or accounts
- No dependency management on your behalf
- No passthrough proxy. A request matching no route is a 404, not a quiet forward to a real backend.
That last one is a deliberate refusal. In the environment this was built for, a mock server that silently reaches out to the real thing is not a convenience. It is a security incident waiting for a slow afternoon.
What are the security considerations for running Mockr?
Mockr runs your handlers and interceptors as ordinary Node code, in process, with no sandbox, and the management API is unauthenticated. Both servers therefore bind to 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.
How do you get started with Mockr?
Mockr is a free, open source mock API server, MIT licensed and free to use commercially, distributed as a mock API server npm package with no account and no server to maintain.
npx @zethictech/mockr
Bug reports, tests and documentation fixes are all welcome. Larger changes are worth an issue first, since the scope is deliberately narrow.
How does Zethic approach problems like this?
Mockr came out of solving a real constraint for a banking client, not a decision to build tooling for its own sake. When an environment cannot be reached and a payload cannot be read in plain text, a fix has to handle both problems at once or it is not really a fix.
That is the same approach behind engineering work across regulated, high-stakes systems: work backward from the constraint the client is actually operating under, rather than the constraint a generic tool assumes. If you are building against an API you cannot reach either, this is worth a conversation with Zethic.