API Mock Tools: 4 Steps Mock Fetch JSON & Status Codes
Static JSON Files vs. Living Mock Endpoints: Which One Actually Saves Your Sprint?
Picture two frontend developers on the same team. Developer A drops a hardcoded users.json file into /public, imports it, and ships a dashboard that looks perfect — until QA hits a 403 and the whole UI freezes. Developer B wires up a service worker that intercepts fetch calls, returns dynamic JSON placeholders, and simulates custom status codes on demand. Same sprint. Same feature. Wildly different outcomes.
If you've ever stared at a spinning loader because the backend endpoint you need is "almost ready," you already know why API mock tools exist. But there's a massive gap between static mock data and a service worker setup that actually behaves like a real server. This walkthrough is for frontend devs who want to close that gap — without spinning up a separate mock server or installing a 200MB framework.
Let's build it step by step.
Step 1: Skip the Fetch Override — Register a Service Worker Instead
The Wrong Way: Monkey-Patching window.fetch
A lot of tutorials suggest overriding window.fetch at the top of your entry file:
window.fetch = (url) => { /* return mock */ }
It works for five minutes. Then you realize it intercepts everything — including analytics calls, CDN requests, and that font file your design system needs. You end up writing fragile URL filters, and the moment you forget to remove the override, production breaks.
The Right Way: A Dedicated Service Worker
Service workers sit between your app and the network. They only intercept what you tell them to. Create a file called mock-sw.js in your public directory:
// public/mock-sw.js
self.addEventListener('install', (event) => {
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim());
});
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
if (url.pathname.startsWith('/api/')) {
event.respondWith(handleMockRequest(event.request, url));
}
// Everything else passes through untouched
});
That startsWith('/api/') guard is the key. Analytics, fonts, images — none of them get touched. Only your API namespace gets intercepted.
Step 2: Hardcoded Responses vs. Dynamic JSON Placeholders
The Wrong Way: Returning the Same JSON Every Time
Returning a static JSON object is fine for a smoke test. But what happens when you need to render a list of 50 items? Or test pagination? Or verify that your date formatting works when the API returns timestamps from last Tuesday?
You copy-paste. You edit JSON by hand. You waste time.
The Right Way: Generate Placeholders Dynamically
Inside your service worker, build a response generator:
function handleMockRequest(request, url) {
if (url.pathname === '/api/users') {
const page = parseInt(url.searchParams.get('page') || '1');
const users = generateUsers(25, page);
return new Response(JSON.stringify({
data: users,
page,
total: 500,
totalPages: 20
}), {
headers: { 'Content-Type': 'application/json' }
});
}
}
function generateUsers(count, page) {
const users = [];
for (let i = 0; i < count; i++) {
const id = (page - 1) * count + i + 1;
users.push({
id,
name: `Test User ${id}`,
email: `user${id}@mock.dev`,
createdAt: new Date(Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000).toISOString()
});
}
return users;
}
Now your fetch call to /api/users?page=2 returns page 2. Page 3 returns page 3. The createdAt field is a random date within the last year — so your "2 days ago" relative timestamp component actually gets exercised.
This is what separates a real API mock tool from a JSON file sitting in a folder.
Step 3: Only Testing 200 OK vs. Simulating Custom Status Codes
The Wrong Way: Assuming Everything Succeeds
Here's a number that should make you uncomfortable: in a typical production API, roughly 15-20% of requests don't return 200. That includes 401 session expirations, 403 permission denials, 404 missing resources, 422 validation errors, and the dreaded 500. If your frontend only ever sees 200s during development, you're shipping untested error paths to real users.
The Right Way: Map Routes to Custom Status Codes
Extend your handler to return specific status codes based on the route, query params, or even a header you set for testing:
function handleMockRequest(request, url) {
const simulateError = url.searchParams.get('_status');
if (url.pathname === '/api/users') {
const users = generateUsers(25, 1);
const status = simulateError ? parseInt(simulateError) : 200;
return new Response(
status === 204 ? null : JSON.stringify({
data: users,
error: status >= 400 ? getErrorBody(status) : undefined
}),
{
status,
headers: { 'Content-Type': 'application/json' }
}
);
}
if (url.pathname === '/api/auth/login') {
const status = simulateError || 200;
if (status === 401) {
return new Response(JSON.stringify({
error: 'Invalid credentials',
code: 'AUTH_FAILED'
}), { status: 401, headers: { 'Content-Type': 'application/json' } });
}
return new Response(JSON.stringify({
token: 'mock-jwt-token-abc123',
user: { id: 1, name: 'Dev User' }
}), { status: 200, headers: { 'Content-Type': 'application/json' } });
}
}
function getErrorBody(status) {
const messages = {
400: 'Bad Request',
401: 'Unauthorized',
403: 'Forbidden',
404: 'Not Found',
422: 'Validation Error',
500: 'Internal Server Error'
};
return { error: messages[status] || 'Unknown Error', status };
}
Now you can trigger any status code by appending ?_status=403 to your API URL. Want to see how your login form handles a 401? Just hit /api/auth/login?_status=401. Want to test your 500 error page? ?_status=500.
Step 4: Manual Registration vs. Conditional Dev-Only Setup
The Wrong Way: Registering the Service Worker Unconditionally
If you register your mock service worker in your main bundle without a guard, it goes to production. Your users start receiving mock data. Tickets get filed. Trust erodes.
The Right Way: Gate Registration Behind an Environment Check
In your app's entry point, register the mock worker only when you're in development:
// src/main.js (or equivalent)
if (process.env.NODE_ENV === 'development') {
navigator.serviceWorker
.register('/mock-sw.js')
.then(() => console.log('[mock] service worker active'))
.catch((err) => console.warn('[mock] registration failed', err));
}
Better yet, add a toggle so you can disable mocks on demand during development — say, when the backend finally ships and you want to test against the real thing:
const ENABLE_MOCKS = true; // flip to false to hit real APIs
if (process.env.NODE_ENV === 'development' && ENABLE_MOCKS) {
navigator.serviceWorker.register('/mock-sw.js');
}
Step 5: Forgetting Edge Cases vs. Covering the Real-World Checklist
The Wrong Way: Mocking Only GET Requests
Most mock setups handle GET and stop there. But your app sends POST, PUT, PATCH, and DELETE requests too. If your service worker ignores them, those requests hit the network and fail — or worse, hit a real endpoint and mutate data.
The Right Way: Handle Every HTTP Method
Here's your checklist for a complete mock setup:
- GET — returns dynamic lists and single resources
- POST — reads the request body, returns a 201 with the created resource (including a generated ID)
- PUT/PATCH — reads the body, returns the updated resource
- DELETE — returns 204 No Content
- Network latency — add a small delay so your loading states actually render
- CORS headers — include them so your app doesn't throw cross-origin errors
Here's how to add latency and handle POST:
async function handleMockRequest(request, url) {
// Simulate realistic network latency (100-300ms)
await new Promise(resolve => setTimeout(resolve, 100 + Math.random() * 200));
if (request.method === 'POST' && url.pathname === '/api/users') {
const body = await request.json();
return new Response(JSON.stringify({
id: Math.floor(Math.random() * 10000),
...body,
createdAt: new Date().toISOString()
}), {
status: 201,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
}
if (request.method === 'DELETE') {
return new Response(null, { status: 204 });
}
}
That 100-300ms delay matters more than you'd think. Without it, your mock responses resolve so fast that skeleton loaders never appear — and you ship a UI where loading states are visually broken because nobody ever saw them during development.
The Before and After
Before this setup: you hardcode JSON, test only happy paths, can't simulate errors, and your loading states are untested. After: every API call is intercepted, responses are dynamic, status codes are controllable via query params, and your mock layer never touches production.
The service worker approach isn't the only way to mock fetch calls — there are libraries like MSW that do heavy lifting for you. But if you want something lightweight, transparent, and fully under your control, a hand-rolled service worker gives you exactly that. No dependencies. No build step. Just a single file that makes your frontend development loop dramatically faster.
Copy the snippets above into your project, flip that ENABLE_MOCKS flag to true, and start building against an API that actually behaves like one.
Frequently Asked Questions
How do I mock fetch API requests using service workers?
You can mock fetch API requests by registering a service worker in your browser that intercepts outgoing network requests. Tools like Mock Service Worker (MSW) allow you to define response handlers that return custom JSON data without hitting a real backend. This approach enables seamless local frontend development and testing.
How can I return custom HTTP status codes when mocking APIs?
When using service worker-based mocking tools like MSW, you can easily specify custom status codes within your response handlers. Simply set the status property in your response object to simulate scenarios like 404 Not Found, 500 Internal Server Error, or 401 Unauthorized. This helps you test how your frontend handles various API error states effectively.
How do I generate dynamic JSON placeholders for mocked API responses?
You can generate dynamic JSON placeholders by integrating data generation libraries like Faker.js with your mock API service. Instead of returning static JSON, your service worker response handler can dynamically create randomized names, dates, and IDs for each request. This ensures your frontend UI is tested against realistic and varied data sets.
What is the best tool to mock APIs with service workers?
Mock Service Worker (MSW) is widely considered the best tool for mocking APIs via service workers in local frontend development. It is framework-agnostic, works seamlessly with REST and GraphQL, and intercepts requests at the network level. This makes it highly reliable because it does not require overriding global fetch functions or modifying your application code.
How do I intercept fetch requests in the browser for testing?
To intercept fetch requests in the browser, you need to register a service worker that listens to network fetch events. Once registered, the service worker acts as a middleware, capturing specific API endpoints and returning mocked JSON responses instead of making actual network calls. This allows you to develop your frontend UI independently of the backend.
Can I mock API responses conditionally based on request parameters?
Yes, advanced API mock tools allow you to read request parameters, query strings, or body payloads before returning a mocked response. You can write custom logic in your service worker handler to return different JSON placeholders or status codes based on the incoming request data. This is highly useful for testing complex frontend workflows like search filtering or pagination.
How to simulate network latency in mocked API requests?
Most service worker mocking libraries allow you to introduce artificial delays in your response handlers to simulate network latency. By wrapping your mocked JSON response in a setTimeout function or using a built-in delay option, you can test your frontend's loading states. This ensures your UI provides proper feedback during slow API requests.
Why use service workers for API mocking instead of mocking libraries like Axios Mock Adapter?
Service workers intercept requests at the browser network level, meaning they work regardless of the HTTP client library you use, such as fetch, Axios, or XMLHttpRequest. Unlike library-specific adapters, service workers require zero changes to your application code for testing. This provides a more realistic testing environment that closely mimics actual network behavior.
How do I set up Mock Service Worker for local frontend development?
To set up MSW, you first install the library and run the initialization command to generate the service worker file in your public directory. Then, you define your request handlers with custom JSON and status codes, and start the worker in your application's entry point. Once running, it will automatically intercept your specified API endpoints during local development.