Greetings, gentlehumen! Today i want to tell you about Mock Service Worker - a powerful framework-agnostic library that allows you to write complete frontend code even without a real backend
When do we need it
- If your frontend team is developing the UI simultaneously or even before backend, but API contracts are already defined
- If your backend team can't yet implement functionality required for testing or showcasing the frontend
- If you want to create a temporary (or not) proxy for API calls
- If you need to test scenarios that require specific data, but the backend can't provide it yet
How does it work?
MSW uses a Service Worker to intercept and mock HTTP requests at the network level.
If a request matches a mock handler, MSW returns the fake or modified response. If not, the request goes through to the real backend (unless told otherwise).
Setting up project
To get started, you just need to create anything that can be considered a webpage - whether it's a React, Angular, Vue app, or even a static HTML page.
For example i'll create a React app using Vite.
You can find the full example on GitHub
MSW installation
Step 1: Install
Now that we have a project ready, let’s add API mocking with MSW.
You can install it using your preferred package manager:
npm or bun
npm i msw --save-dev
or
bun i msw --save-dev
Or by inserting script into your HTML code:
unpkg
<script src="https://unpkg.com/msw/lib/iife/index.js"></script>
jsDelivr
<script src="https://cdn.jsdelivr.net/npm/msw/lib/iife/index.js"></script>
Step 2: Generating mock file
Next, generate the service worker script:
npx msw init public/ --save
(You can specify a different folder instead of public/ if needed)
Step 3: Basic configuration
Now that MSW is installed, let’s configure it.
Create a src/mocks
directory and add a file named handlers.ts
:
// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw'
export const handlers = []
Then, set up the worker:
// src/mocks/browser.ts
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'
export const worker = setupWorker(...handlers)
Now, enable mocking when your app starts. Modify src/main.tsx like this:
// src/main.tsx
async function enableMocking() {
if (import.meta.env.DEV) {
const { worker } = await import("./mocks/browser.ts")
return await worker.start()
}
}
// Also add here:
enableMocking().then(() => {
createRoot(document.getElementById("root")!).render(
<StrictMode>
<BrowserRouter>
<Routes>
<Route path="/" element={<App />} />
</Routes>
</BrowserRouter>
</StrictMode>
)
})
And now MSW is set up and ready to mock requests.
Mocking backend
Let's sat we're building a frontend for a "films to watch" list website, but the backend team hasn't started development yet.
Instead of waiting, we want to implement and test production-ready frontend - with full functionality, loading states and proper error handling.
To simulate a films database, we can use a simple array
or Map
. If you want to persist data between page reloads, you can also use localStorage
or IndexedDB (e.g. with Dexie).
For this example, we’ll assume the backend will be hosted at:
http://localhost:3100
Let’s add our “mock database” to handlers.ts
:
// src/types/film.d.ts
type Film = {
id: string
name: string
description: string
rating: number
}
// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw'
// our "database"
const films = new Map([
[1, { id: "1", name: "Test film", description: "Test film", rating: 5 }],
])
const backendUrl = `http://localhost:3100`
export const handlers = []
Mock GET
To mock Get endpoint you just need to add this method into handlers array:
http.get(`${backendUrl}/films`, () => {
const filmsValues = Array.from(films.values())
const filmsSummary = filmsValues.map((film) => {
const { description, rating, ...strippedFilm } = film
return strippedFilm
})
return HttpResponse.json(filmsSummary)
}),
And then you can fetch localhost:3100/films
to get films array summary
To mock endpoint with params you'll just need to add param name with ":" into link:
http.get(`${backendUrl}/films/:filmId`, ({ params }) => {
const filmId =
typeof params.filmId === "string" ? Number(params.filmId) : NaN
if (!Number.isInteger(filmId)) {
return HttpResponse.json({ message: "Invalid film ID" }, { status: 400 })
}
const film = films.get(filmId)
return film
? HttpResponse.json(film)
: HttpResponse.json({ message: "Film not found" }, { status: 404 })
}),
Network errors
To simulate network errors you can return HttpResponse.error()
with a certain chance or if you want to add reason, you can send response like this: HttpResponse.json({ message: "Film not found" }, { status: 404 })
function getIsError() {
return Math.random() > 0.75
}
http.get(`${backendUrl}/films`, () => {
const filmsValues = Array.from(films.values())
const filmsSummary = filmsValues.map((film) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { description, rating, ...strippedFilm } = film
return strippedFilm
})
return getIsError() ? HttpResponse.error() : HttpResponse.json(filmsSummary) // Return error with 25% chance
}),
Mock POST & PUT & PATCH & DELETE
Mocking endpoints that change data is as easy as mocking GET requests. Here is an example:
http.post(`${backendUrl}/films`, async ({ request }) => {
const film = (await request.json()) as Omit<Film, "id">
const newId = films.size > 0 ? Math.max(...films.keys()) + 1 : 1
const newFilm: Film = {
...film,
id: String(newId),
}
films.set(newId, newFilm)
return HttpResponse.json(newFilm, { status: 201 })
}),
Data persistence problem across different pages and how to deal with it
Handlers lose their variables on page reload / navigation, so if you want to persist data between page loadings you need to use localstorage
or indexeddb
Cookies
To work with Cookies you just need to destructure object in callback argument:
http.get(`${backendUrl}/cookies`, ({ cookies }) => {
return HttpResponse.json(cookies)
}),
Query
To read search params (query), you need to create a new Url instance from the request url, and get them using method:
http.get(`${backendUrl}/query`, ({ request }) => {
const url = new URL(request.url)
const id = url.searchParams.get("id")
return HttpResponse.json(id)
}),
Respone patching (proxy)
Mock service workers are also capable of proxying real requests to modify or track them:
http.get('${backendUrl}/user', async ({ request }) => {
const user = await fetch(bypass(request)).then((response) =>
response.json()
)
// or add tracking code, that writes data into localstorage or indexeddb
return HttpResponse.json({
id: user.id,
name: user.name,
role: "admin"
})
}),
Or you can proxy outgoing requests:
http.get("${backendUrl}/dashboard", async ({ request }) => {
const proxyUrl = new URL("/proxy", location.origin)
const proxyRequest = new Request(proxyUrl, {
headers: {
"Content-Type": request.headers.get("content-type") || "application/json",
"X-Proxy-Header": "true",
},
})
const originalResponse = await fetch(bypass(proxyRequest))
return HttpResponse.json(originalResponse)
}),
Conclusion
Service workers let you intercept network requests, cache data, and make your app work offline. Mock Service Worker (MSW) uses this power to help you mock API requests in a simple way. With MSW, you can build and test your frontend without needing a real backend, which helps you work faster and avoid issues from unstable or unfinished APIs.
Top comments (0)