DEV Community

Cover image for The Backend Is Late — But Your Frontend Doesn’t Have to Be
FalloutEngineer
FalloutEngineer

Posted on

The Backend Is Late — But Your Frontend Doesn’t Have to Be

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
Enter fullscreen mode Exit fullscreen mode

or

bun i msw --save-dev
Enter fullscreen mode Exit fullscreen mode

Or by inserting script into your HTML code:

unpkg

<script src="https://unpkg.com/msw/lib/iife/index.js"></script>
Enter fullscreen mode Exit fullscreen mode

jsDelivr

<script src="https://cdn.jsdelivr.net/npm/msw/lib/iife/index.js"></script>
Enter fullscreen mode Exit fullscreen mode

Step 2: Generating mock file

Next, generate the service worker script:

npx msw init public/ --save
Enter fullscreen mode Exit fullscreen mode

(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 = []
Enter fullscreen mode Exit fullscreen mode

Then, set up the worker:

// src/mocks/browser.ts
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'

export const worker = setupWorker(...handlers)
Enter fullscreen mode Exit fullscreen mode

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>
  )
})
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Let’s add our “mock database” to handlers.ts:

// src/types/film.d.ts
type Film = {
  id: string
  name: string
  description: string
  rating: number
}
Enter fullscreen mode Exit fullscreen mode
// 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 = []
Enter fullscreen mode Exit fullscreen mode

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)
}),
Enter fullscreen mode Exit fullscreen mode

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 })
}),
Enter fullscreen mode Exit fullscreen mode

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
}),

Enter fullscreen mode Exit fullscreen mode

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 })
}),
Enter fullscreen mode Exit fullscreen mode

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)
}),
Enter fullscreen mode Exit fullscreen mode

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)
}),
Enter fullscreen mode Exit fullscreen mode

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"
  })
}),
Enter fullscreen mode Exit fullscreen mode

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)
}),
Enter fullscreen mode Exit fullscreen mode

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)