How to use Mocking Service Worker (MSW) for your JS POC demos.
Prerequisites
Live Demo - give it a minute to load, I am using free hosting;
GitHub Repo with an example - https://github.com/zhivou/msw-demo?tab=readme-ov-file;
I am going to use Node for simplicity but it can be any js front-end framework - https://nodejs.org/en;
Mocking Service Worker - https://mswjs.io;
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.Follow https://mswjs.io/docs/getting-started MSW starter guide;
locate your public folder and place
mockServiceWorker.js
the script inside. For detailed instructions visit - https://mswjs.io/docs/best-practices/managing-the-worker;Decide where you want to keep your Handlers and Data;
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.
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.