Safeguard
Best Practices

Mocking APIs for secure testing: MSW and json-server for error and auth flows

MSW intercepts requests at the network layer; json-server spins up a fake REST API in one command. Neither should ever touch a real backend or real credentials.

Safeguard Research Team
Research
7 min read

Most client-side bugs that ship to production are not rendering bugs — they are error-handling bugs: the app that hangs forever on a 500, the token-refresh loop that never fires on a 401, the form that silently swallows a malformed payload. Reproducing those states against a real backend is slow and often impossible, since you'd need to coax a production auth server into issuing an expired token or a live API into returning a 503 on demand. Mock Service Worker (MSW), an open-source library at github.com/mswjs/msw that its maintainers describe as "industry standard API mocking for JavaScript," solves this by intercepting fetch and XHR calls at the network level — in the browser via the Service Worker API, and in Node via its @mswjs/interceptors layer — so application code runs unmodified while the response is faked. json-server, an actively maintained npm package with roughly 311,000 weekly downloads per Socket.dev's package analysis, takes a different approach: point it at a JSON file and it stands up a full fake REST API in one command. Both tools let teams deterministically simulate 4xx/5xx errors, timeouts, and auth edge cases that are unsafe or unreliable to trigger against production systems. This post covers how to use each one, and the credential-hygiene mistakes that turn a helpful mock into a real exposure.

What does MSW actually intercept, and why does that matter for testing?

MSW intercepts requests at the network boundary rather than by monkey-patching fetch or wrapping your HTTP client, which means the exact same mock definitions work in unit tests, in Node-based integration tests, and live in the browser during local development or demos. In the browser it registers a real Service Worker that sits between your app and the network; in Node it uses @mswjs/interceptors to hook into the underlying request machinery. Because interception happens below your application code, you never need "if test mode, use a fake client" branches — the app calls fetch("/api/user") exactly as it would in production, and MSW decides whether that request reaches the network or gets answered by a handler you defined. MSW also ships an official Playwright integration (mswjs/playwright) for mocking API responses inside end-to-end browser tests, extending the same handler definitions from unit tests into full e2e runs without a second mocking layer.

How do you simulate a broken or slow backend with MSW?

You define a handler per endpoint and return whatever response, status, or delay you want to test against. A handler for a failed login might match a POST to /api/login and return a 401 with a JSON error body; a handler for a flaky search endpoint might return a 503 on the first call and a 200 on retry, to verify your app's retry logic actually retries. MSW's handlers can also introduce artificial latency or simulate a network failure outright, which is the only reliable way to test a loading spinner's timeout behavior or a "request cancelled" code path — real backends rarely fail exactly when you need them to for a test run. This pattern replaces older approaches like manually stopping a local server mid-test or mocking window.fetch directly, both of which couple the test to implementation details rather than to the observable request/response contract.

How does json-server differ from MSW, and when is it the better tool?

json-server differs from MSW by running as an actual standalone HTTP server backed by a JSON file, rather than intercepting calls inside your test runtime — you start it with a single command and it exposes full CRUD routes (GET, POST, PUT, DELETE) generated automatically from your data's shape. That makes it a better fit for front-end prototyping before a real backend exists, for manual exploratory testing in a browser tab, or for integration tests that want to exercise real HTTP round-trips (headers, status codes, actual network timing) rather than an in-process interception layer. MSW is generally the better fit inside a test suite itself, since handlers live alongside the test code and can be overridden per-test without restarting a server process. The two are not mutually exclusive: some teams use json-server for local development against a stable fake dataset and MSW for the fine-grained, per-test error scenarios that a static JSON file can't express — a 401 on the third request but not the first, for example.

Why is mocking the only safe way to test auth failure paths?

Mocking is the only safe way to test auth failure paths because deliberately triggering expired tokens, invalid signatures, or repeated 401/403 responses against a real identity provider risks account lockouts, rate-limit bans, or tripping fraud/anomaly detection meant to catch exactly that behavior. A mocked auth flow lets you assert, deterministically, that your app redirects to login on a 401, retries once with a refreshed token before giving up, and never enters an infinite refresh loop when the refresh endpoint itself returns 401 — a real bug class in token-refresh implementations that only shows up under repeated failure, not the happy path. Testing this against a live third-party API (an OAuth provider, a payment processor, an internal SSO service) also means every CI run makes real calls against infrastructure you don't fully control, which is slow, flaky, and can violate a provider's terms of service around automated testing traffic.

What credential-hygiene mistakes turn a mock into a real exposure?

The most common mistake is putting a real API key, bearer token, or database credential into a mock definition "just to make it look realistic," because mock handlers and fixture files routinely get committed to source control and reviewed by anyone with repo access. A hardcoded Authorization: Bearer sk_live_... string in a test file is indistinguishable, to a secrets scanner or a human diffing a PR six months later, from a genuine leaked credential — and if it happens to be a real key rather than a placeholder, it is one. The fix is procedural, not technical: mock fixtures should only ever contain obviously fake values (test-token-123, fake@example.com) that fail conspicuously if anyone ever tries to use them against a real service. This is also why mocking and dynamic testing against real, verified infrastructure are deliberately separate disciplines — Safeguard's DAST capability, for instance, only runs active checks against a target after ownership is proven (via DNS TXT record, file upload, meta tag, or verified email domain) and enforces strict scope and rate limits on every request, precisely because testing against a real system carries risk that client-side mocking is designed to avoid entirely.

How Safeguard Helps

Safeguard doesn't mock your APIs — that's rightly a front-end testing concern — but it does help make sure the fixtures and test code that surround MSW and json-server setups don't quietly become a supply-chain or secrets problem. Safeguard's secrets scanning runs across every commit to catch a real credential that slipped into a fixture file before it reaches a shared branch, and its SCA engine tracks the msw and json-server packages themselves in your dependency tree, flagging known CVEs or unmaintained releases the same way it would any other dependency. And where a team's real testing need moves from mocking client behavior to validating a live, running application, Safeguard's DAST capability picks up exactly where mocking leaves off — requiring verified target ownership and enforcing scope and rate limits before a single active request runs, so the transition from "safe fake backend" to "safe test against a real one" never skips a safety control.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.