Intercept Fetch API with Service Workers | API Mock Tools

It's 2 AM and Your Backend Team Just Went to Bed

Your sprint demo is tomorrow morning. The frontend is 90% complete, but the new payment endpoint your entire checkout flow depends on? It's returning a 503. The backend team is asleep, and you're staring at a broken integration that needs to work by 9 AM.

We've all been there. You need to test against an API that doesn't exist yet, is down, or requires authentication tokens you don't have. This is where knowing how to intercept fetch API requests with service workers becomes your late-night superhero skill.

The Problem: Why Traditional API Mocking Falls Short

Most developers reach for one of three solutions when they need mock data, and each one has a frustrating limitation.

The Environment Variable Swap

You create a USE_MOCKS=true flag and conditionally return fake data. It works, but now your production code is littered with mock logic. Every component has if (process.env.USE_MOCKS) scattered throughout, making the codebase harder to maintain and debug.

The Proxy Server Approach

You spin up a separate mock server using Express or json-server. Now you're managing two processes, configuring CORS, and dealing with port conflicts. Your team lead asks why localhost:3001 needs to be running for the app to work locally.

The Manual Fetch Override

You override window.fetch at the top of your entry file. It feels hacky, breaks when third-party libraries also override fetch, and you forget to remove it before shipping to production. One developer actually shipped mock data to staging because of this.

The real issue? None of these solutions intercept requests at the network level. They either modify application code or require external infrastructure. What you need is something that sits between your app and the network, transparently intercepting requests without touching your business logic.

The Cause: Understanding the Service Worker Architecture

Service workers are JavaScript files that run in a separate thread from your main page. They act as a programmable network proxy, sitting between your web application and the network. When registered, they can intercept fetch requests, cache responses, and serve custom content—all without modifying your application code.

Here's why this matters for API mocking: service workers intercept requests at the browser level. Your application code stays clean. Your fetch('https://api.example.com/users') call remains unchanged. The service worker simply catches it before it hits the network and returns whatever mock response you define.

The key event is fetch. When a service worker is active, every network request from your page triggers a fetch event in the service worker. You can listen for this event, inspect the request URL, and respond with a custom Response object.

One critical detail: service workers only work in secure contexts. That means HTTPS in production, but localhost is treated as a secure context for development purposes. This makes it perfect for local API mocking.

The Solution: Building a Service Worker Fetch Interceptor

Let's build a practical service worker that intercepts fetch requests and returns mock data. We'll go step by step.

Step 1: Create the Service Worker File

Create a file called mock-sw.js in your public directory. This file must be served from the root path or a subdirectory of your origin.

// mock-sw.js
const mockResponses = {
  '/api/users': {
    status: 200,
    body: JSON.stringify([
      { id: 1, name: 'Alice', email: 'alice@example.com' },
      { id: 2, name: 'Bob', email: 'bob@example.com' }
    ])
  },
  '/api/orders': {
    status: 200,
    body: JSON.stringify([
      { id: 101, total: 49.99, status: 'shipped' },
      { id: 102, total: 129.00, status: 'processing' }
    ])
  }
};

self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);
  
  // Check if this request should be mocked
  if (mockResponses[url.pathname]) {
    const mock = mockResponses[url.pathname];
    const response = new Response(mock.body, {
      status: mock.status,
      headers: { 'Content-Type': 'application/json' }
    });
    event.respondWith(response);
    return;
  }
  
  // Pass through non-mocked requests
  event.respondWith(fetch(event.request));
});

Step 2: Register the Service Worker

In your application's entry point, register the service worker. Only do this in development to avoid shipping mocks to production.

// app.js or main.js
if (process.env.NODE_ENV === 'development') {
  navigator.serviceWorker
    .register('/mock-sw.js')
    .then((reg) => console.log('Mock SW registered:', reg.scope))
    .catch((err) => console.error('Mock SW registration failed:', err));
}

Step 3: Handle Dynamic Routes and Query Parameters

Real APIs have dynamic segments like /api/users/123. Let's enhance our interceptor with pattern matching.

// mock-sw.js (enhanced)
const mockPatterns = [
  {
    pattern: /^\/api\/users\/(\d+)$/,
    handler: (match, request) => {
      const userId = match[1];
      return new Response(
        JSON.stringify({ id: userId, name: 'Mock User', email: 'mock@example.com' }),
        { status: 200, headers: { 'Content-Type': 'application/json' } }
      );
    }
  },
  {
    pattern: /^\/api\/products$/,
    handler: (match, request) => {
      // Simulate network delay of 250ms
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve(new Response(
            JSON.stringify([
              { id: 1, name: 'Widget', price: 19.99 },
              { id: 2, name: 'Gadget', price: 39.99 }
            ]),
            { status: 200, headers: { 'Content-Type': 'application/json' } }
          ));
        }, 250);
      });
    }
  }
];

self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);
  
  for (const mock of mockPatterns) {
    const match = url.pathname.match(mock.pattern);
    if (match) {
      event.respondWith(mock.handler(match, event.request));
      return;
    }
  }
  
  event.respondWith(fetch(event.request));
});

Step 4: Support POST, PUT, and DELETE Requests

Mocking isn't just about GET requests. You need to handle mutations too.

self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);
  const method = event.request.method;
  
  if (url.pathname === '/api/users' && method === 'POST') {
    event.respondWith(
      event.request.json().then((body) => {
        return new Response(
          JSON.stringify({ id: Date.now(), ...body }),
          { status: 201, headers: { 'Content-Type': 'application/json' } }
        );
      })
    );
    return;
  }
  
  // ... other mock handlers
});

Step 5: Toggle Mocks Without Code Changes

Hardcoding mocks in the service worker file gets unwieldy. A better approach: load mock definitions from a separate JSON file or use a query parameter to enable/disable mocking on the fly.

self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);
  
  // Skip mocking if disabled via query param
  if (url.searchParams.get('nomock') === 'true') {
    event.respondWith(fetch(event.request));
    return;
  }
  
  // ... rest of mock logic
});

Now visiting http://localhost:3000?nomock=true bypasses all mocks instantly. No rebuild required.

Common Pitfalls and How to Avoid Them

The Service Worker Won't Update

Service workers are aggressively cached by the browser. When you change mock-sw.js, the browser checks for updates by comparing byte-for-byte. If you're not seeing changes, close all tabs using that service worker, or use self.skipWaiting() in the install event.

Scope Issues

A service worker registered at /mock-sw.js controls the entire origin. If your API calls go to a different origin, the service worker won't intercept them. You can only intercept same-origin requests unless you handle CORS properly in your mock responses.

Forgetting to Unregister

This happens more often than you'd think. A developer registers a mock service worker, finishes their feature, and pushes to production. The service worker stays active, serving stale mock data to real users. Always gate registration behind an environment check, and provide a way to unregister:

navigator.serviceWorker.getRegistrations()
  .then(regs => regs.forEach(r => r.unregister()));

When to Reach for Dedicated API Mock Tools

Hand-rolling a service worker interceptor works well for small projects or quick prototypes. But as your mock definitions grow—say, 50+ endpoints with complex response logic—maintaining raw JavaScript objects becomes painful.

Dedicated API mock tools built on service workers handle the boilerplate for you. They provide declarative mock definitions, request recording for replay, and automatic cleanup. Tools like MSW (Mock Service Worker) implement exactly the pattern we built above, but with a polished API and active maintenance.

The underlying mechanism is the same: a service worker intercepts fetch requests and returns mock responses. Whether you build it yourself or use a tool, understanding the fetch interception pattern gives you full control over your local development environment.

Next time the backend is down at 2 AM, you won't panic. Your service worker will quietly serve the mock responses you need, and your demo will work flawlessly come morning.

Frequently Asked Questions

How do I intercept fetch API requests with a service worker?

You can intercept fetch API requests by registering a service worker and adding a `fetch` event listener inside it. When a network request is made, the service worker captures it, allowing you to respond with custom mock data instead of hitting the actual server. This is done using the `event.respondWith()` method to return a mocked `Response` object.

Can service workers be used for local API mocking?

Yes, service workers are excellent for local API mocking because they act as a programmable network proxy between your web application and the network. By intercepting outgoing requests, you can serve local JSON files or predefined responses, enabling full frontend development without a working backend.

How do I set up a service worker to mock API requests?

To set up a service worker for mocking, first register it in your main JavaScript file using `navigator.serviceWorker.register()`. Inside the service worker file, attach an event listener to the `fetch` event and use `event.respondWith()` to return your mocked data. Make sure the service worker file is served from the correct directory scope to control your application's network requests.

How do I bypass certain requests in a service worker while mocking others?

Inside your `fetch` event listener, you can check the `event.request.url` to determine if it matches the API endpoints you want to mock. If it matches, return your mocked response; otherwise, call `fetch(event.request)` to let the request pass through to the network normally. This allows you to mock specific APIs while keeping standard asset loading intact.

What are the limitations of using service workers for API mocking?

The main limitation is that service workers only run in secure contexts, meaning your site must be served over HTTPS or localhost. Additionally, they operate asynchronously and cannot intercept synchronous XMLHttpRequests, only modern `fetch` API calls. Service workers also persist across page reloads, so you must actively unregister them to stop the mocking behavior.

How do I mock a POST request using a service worker?

To mock a POST request, you need to check `event.request.method === 'POST'` inside your fetch event listener. You can then parse the request body if needed and return a custom mocked response using `new Response(JSON.stringify(mockData), { status: 200 })`. This allows you to easily simulate form submissions or data mutations locally.

Why use Mock Service Worker (MSW) instead of a custom service worker for mocking?

Mock Service Worker (MSW) is a library that utilizes service workers under the hood to intercept API requests, saving you from writing raw service worker code. Using a dedicated tool like MSW is generally preferred because it handles setup, routing, and edge cases automatically. It provides a cleaner, more maintainable API for defining your mocks without dealing with low-level browser events.

How do I unregister a service worker after API mocking?

You can unregister a service worker by calling `navigator.serviceWorker.getRegistrations()` in your main app script and then calling `.unregister()` on each registration. Alternatively, you can do this directly in the browser's DevTools under the Application tab. Unregistering the worker ensures that your network requests are no longer intercepted and normal API routing is restored.

Does service worker API mocking work in all browsers?

Service workers are widely supported across all modern browsers, including Chrome, Firefox, Edge, and Safari. However, they are not supported in older browsers like Internet Explorer. As long as your development environment or users are using an up-to-date browser, fetch interception via service workers will work reliably.