How to use Mocking Service Worker (MSW) for your JS POC demos.

Dmitrii SkrylevDmitrii Skrylev
6 min read
👉
Check out my project MudQuest the first real-time offroad enthusiast finder.

Prerequisites

What is MSW(Mock Service Worker)

Mock Service Worker is an API mocking library that allows you to write client-agnostic mocks and reuse them across any frameworks, tools, and environments.

Sometimes we need a working application to record POC demos or videos, but we don't have a fully developed API yet. Or, we might want to change the results to make a better impression. MSW is the perfect tool to help with that. In this post I am going to share with you how you can create UI-driven API without actual backend by using you already guessed - MSW! So let’s dive right in!

When to use it

  • Demos. When you want to start designing your API structure but are not yet sure of the final result, this is the best way to quickly create endpoints filled with demo data and present it with various scenarios.

  • Data patching or masking. For example, you want to share some of your data but wish to anonymize it;

  • Testing;

  • Serverless Developing mode;

  • Reproducing or setting scenarios;

  • Can be adapted well with API-first(API-driven development);

Service Worker

Do not confuse a Mock Service Worker (MSW) with a Service Worker. MSW is the name of a tool, while a Service Worker is a script that the browser runs in the background. However, MSW does use the Service Worker to intercept calls.

With MSW, you can intercept outgoing requests, observe them, and respond to them using mocked responses and it does it by establishing a Service Worker in your Browser or in a Node(out of scope) environment. This means that the end client doesn't realize the call was mocked, making it feel very natural to the front end.

To better understand how it works, I will use a Node server where I am going to build API with MSW handlers + SEEDing data;

Handler example. As you can see, this looks very much like a real API index endpoint. It will intercept all calls matching the /posts URL and return an array of data from our shared allPosts state map (more about it later).

import { postsData } from '../data/posts-data' // seed data

let allPosts = Map(postsData.map(post => [post.id, post]));

// INDEX
http.get('/posts', () => {
  console.log('GET /posts', Array.from(allPosts.entries()));
  return HttpResponse.json(Array.from(allPosts.values()))
}),

The MSW script will pick up this instruction and start intercepting all /posts GET calls. In my example, you can see how the MSW script intercepts it:

We can add all CRUD actions to complete the general loop and make the front end fully functional with the mocked API. Before we do that, let's learn how to set this up.

How to Setup MSW

mockServiceWorker.js should never be deployed to production! It can consume a lot of memory and may pose a security risk. You can keep the script in git but don’t run it.

Add your handlers similarly to mine:

// src/msw/handlers/post.js

import { http, HttpResponse } from 'msw'
import { Map } from 'immutable'
import { postsData } from '../data/posts-data'

/**
 * Mock Service Worker (MSW) handlers for Post-related API endpoints.
 * 
 * This module defines handlers for CRUD operations on posts:
 * - GET /posts: Retrieve all posts
 * - POST /posts: Create a new post
 * - GET /posts/:id: Retrieve a specific post
 * - PUT /posts/:id: Update a specific post
 * - DELETE /posts/:id: Delete a specific post
 * 
 * The handlers use an Immutable.js Map to store posts in memory,
 * simulating a database for testing and development purposes.
 * 
 * @module postHandlers
 */


let allPosts = Map(postsData.map(post => [post.id, post]));

export const handlers = [
    // INDEX
    http.get('/posts', () => {
        console.log('GET /posts', Array.from(allPosts.entries()));
        return HttpResponse.json(Array.from(allPosts.values()))
    }),

    // CREATE
    http.post('/posts', async ({ request }) => {
        const newPost = await request.json();
        const postId = Date.now().toString();
        newPost.id = postId;
        allPosts = allPosts.set(postId, newPost);
        console.log('POST /posts', newPost, Array.from(allPosts.entries()));
        return HttpResponse.json(newPost, { status: 201 });
    }),

    // SHOW
    http.get('/posts/:id', ({ params }) => {
        console.log('GET /posts/:id', params.id, allPosts.has(params.id));
        const post = allPosts.get(params.id);
        if (!post) {
            return new HttpResponse(null, { status: 404 });
        }
        return HttpResponse.json(post);
    }),

    // UPDATE
    http.put('/posts/:id', async ({ params, request }) => {
        console.log('PUT /posts/:id', params.id, allPosts.has(params.id));
        const existingPost = allPosts.get(params.id);
        if (!existingPost) {
            return new HttpResponse(null, { status: 404 });
        }
        const updatedPost = await request.json();
        updatedPost.id = params.id;
        allPosts = allPosts.set(params.id, updatedPost);
        console.log('Updated post', updatedPost);
        return HttpResponse.json(updatedPost);
    }),

    // DELETE
    http.delete('/posts/:id', ({ params }) => {
        console.log('DELETE /posts/:id', params.id, allPosts.has(params.id));
        const post = allPosts.get(params.id);
        if (!post) {
            return HttpResponse.json({ error: 'Post not found' }, { status: 404 });
        }
        allPosts = allPosts.delete(params.id);
        return HttpResponse.json(post);
    }),
];

If you need SEED data you can place it in any location and just load it up to the handlers:

//src/msw/data/posts-data.js
export const postsData = [
  { id: '1', title: 'First Post', content: 'This is the first post', author: 'John Doe' },
  { id: '2', title: 'Second Post', content: 'This is the second post', author: 'Jane Smith' },
];

And this is how you can enable the mocking:

// src/index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { setupWorker } from 'msw/browser'
import { handlers } from './msw/handlers/post';

const root = ReactDOM.createRoot(document.getElementById('root'));

export const worker = setupWorker(...handlers);

async function enableMocking() {
  if (process.env.NODE_ENV !== 'development') {
    return
  }
  return worker.start()
}

enableMocking().then(() => {
  root.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>
  );
})

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

Note how I used let allPosts = Map(postsData.map(post => [post.id, post])); for temporary state management. This will work until you reload the page, so you need to consider how you want to build your demos to ensure the data isn't lost. You can improve this if more complex business logic is needed, such as pagination, dynamic data calculations, or filtering.

If full isolation is not required, you can intercept the request and let it pass through with some updates like email or ID masking. This is beyond the scope of this tutorial, but keep it in mind. You can use SEMI API mocking and build your handlers accordingly. This way, you won't need any state management.

To see how it truly works navigate to my live demo - https://msw-for-demos-1bf324cd6a86.herokuapp.com. <== This may take a minute to load up.

Here, you can see that the app makes API calls, but they are all mocked by MSW within the browser itself:

You can see the full code implementation here.

0
Subscribe to my newsletter

Read articles from Dmitrii Skrylev directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Dmitrii Skrylev
Dmitrii Skrylev

I am indie hacker and entrepreneur. I am focused on building innovative apps like MudQuest for offroad enthusiasts. I’m diving into product development and marketing with a background in testing. Sharing my journey through a blog, I’m committed to turning ideas into reality, one app at a time.