Meet MSW: API Mocking for Modern Development

We’ve all been there: a new project, lots of screens to build, but the backend team is still weeks away from delivering anything usable. The scenario feels all too common.

Having a reliable mocking strategy from day one helps us move forward without waiting, and it makes unit tests more stable by feeding them consistent, known payloads.

You might think this requires complex setups—swapping modules, toggling flags, or dealing with messy scripts just to keep mock code out of production bundles. The truth is, it no longer has to be that painful.

What is MSW?

If you ask the folks at MSW, they call it the API mocking of the next generation. But what does that actually mean in practice? It intercepts network requests through a service worker, operating at the network layer rather than inside your application code.

The best feature might be how transparent this is for developers — everything keeps working normal, as if the real API is responding. And that's a nice benefit.

Scaffolding the Project with NX

We're choosing NX because, well, it's excellent. However, the steps we’re about to walk through will work just as well with a standard Angular CLI workspace.

Start by generating a workspace that includes both an Angular + Nest setup. Mocking the backend isn't a reason to skip having one altogether — it just helps us develop faster in the meantime.

$ npx create-nx-workspace msw-tutorial
Enter fullscreen mode Exit fullscreen mode

During the setup prompt, choose the angular-nest style, name it whatever feels right (I went with spa). For styling, select CSS—we're focusing on functionality, not design—and respond No when asked about cloud setup.

Open this workspace in your preferred editor vscode, and get both projects running simultaneously—the spa and the api:

$ npx nx serve
Enter fullscreen mode Exit fullscreen mode
$ npx nx serve api
Enter fullscreen mode Exit fullscreen mode

You'll need two terminals open for this to work.

Once everything is up, head to http://localhost:4200 and you'll see the following:

Initial Application

You'll spot a Message at the bottom, and it's coming straight from our API. If you're curious, you can peek into apps/api/src/app/app.controller.ts to see how it works.

Configuring MSW in Our Workspace

The app is running correctly, so now we can bring in MSW.

Start by adding it as a dependency:

$ npm i -D msw
Enter fullscreen mode Exit fullscreen mode

Since MSW operates through a service worker, that's what we need to set up next:

$ npx msw init apps/spa/src
Enter fullscreen mode Exit fullscreen mode

This command places the mockServiceWorker.js file directly within the spa project. While it's possible to store it elsewhere, we'll stick with this location for simplicity. When prompted, go ahead and decline saving this path to the package.json — it's not necessary.

Next, we need to make Angular aware of this mockServiceWorker.js. To do that, update angular.json like so:

"options": {
  "outputPath": "dist/apps/spa",
  "index": "apps/spa/src/index.html",
  "main": "apps/spa/src/main.ts",
  "polyfills": "apps/spa/src/polyfills.ts",
  "tsConfig": "apps/spa/tsconfig.app.json",
  "assets": [
    "apps/spa/src/favicon.ico",
    "apps/spa/src/assets",
    "apps/spa/src/mockServiceWorker.js"
  ],
  "styles": [
    "apps/spa/src/styles.css"
  ],
  "scripts": []
},
Enter fullscreen mode Exit fullscreen mode

Angular will then be able to find the service worker correctly when MSW requests its installation.

Now, the big question is: when should we actually use the mocks? We definitely don't want them active in production, and for development, probably not all the time. The convention in many projects is to create a separate environment specifically for this purpose, and we'll name it mock.

To support that, another update in angular.json adds this new configuration:

"development": {
  "buildOptimizer": false,
  "optimization": false,
  "vendorChunk": true,
  "extractLicenses": false,
  "sourceMap": true,
  "namedChunks": true
},
"mock": {
  "buildOptimizer": false,
  "optimization": false,
  "vendorChunk": true,
  "extractLicenses": false,
  "sourceMap": true,
  "namedChunks": true,
  "fileReplacements": [
    {
      "replace": "apps/spa/src/environments/environment.ts",
      "with": "apps/spa/src/environments/environment.mock.ts"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

You can think of this as a clone of the development configuration, but it references a new environment.mock.ts file. So let's go ahead and create that file inside apps/spa/src/environments:

File: environment.mock.ts

export const environment = {
  production: false,
};
Enter fullscreen mode Exit fullscreen mode

To make it simpler to run, let's add a convenient script to our package.json:

File: package.json

"scripts": {
    "ng": "nx",
    "postinstall": "node ./decorate-angular-cli.js && ngcc --properties es2015 browser module main",
    "nx": "nx",
    "start": "ng serve",
    "start-mock": "ng serve spa --configuration mock",
    "build": "ng build",
Enter fullscreen mode Exit fullscreen mode

As a final piece of setup, we need to register this new mock configuration in angular.json so we can actually serve the app with it:

"development": {
  "browserTarget": "spa:build:development"
},
"mock": {
  "browserTarget": "spa:build:mock"
}
Enter fullscreen mode Exit fullscreen mode

Setting Up MSW Configuration

With the environment ready, it's time to build the actual mock. Given that we're working within an NX workspace, the cleanest approach is to generate a dedicated library for this purpose:

$ npx nx g @nrwl/workspace:library --name=mock-api --skipBabelrc --unitTestRunner=none

After creating the library, remove the generated libs/mock-api/src/lib/mock-api.ts file and replace it with the following two files:

File: handlers.ts

export const handlers = [];

File: browser.ts

import { setupWorker } from 'msw';
import { handlers } from './handlers';

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

Don't forget to refresh the barrel file at libs/mock-api/src/index.ts so the new exports are available:

export * from './lib/browser';

In handlers, we define every network call that should be intercepted. The browser.ts file exports a worker instance, which we'll use to bootstrap MSW alongside those handlers.

Deciding where to activate MSW is straightforward—it should only run when we're in mock mode. That means updating the environment file at apps/spa/src/environments/environments.mock.ts:

import { worker } from '@msw-tutorial/mock-api';

worker.start({
  onUnhandledRequest: 'bypass',
});

export const environment = {
  production: false,
};

Here, we've made a strategic choice: any request not explicitly handled by our mocks is passed straight through to the real backend via the bypass option. This gives us fine-grained control over which endpoints are mocked and which are live.

Next, relaunch both the backend and the frontend:

$ npm run start-mock
$ npx nx serve api

The only difference this time is that we're invoking the new start-mock script.

Visiting http://localhost:4200 will show, unsurprisingly, the same page as before:

Initial Application again

However, a quick look at the browser console reveals something new:

Console showing MSW working

MSW is clearly active and intercepting traffic—we simply haven't registered any handlers yet.

Before proceeding, you might see a console warning about a file that depends on 'debug'. If that pops up, adjust angular.json with the following configuration:

"options": {
  "outputPath": "dist/apps/spa",
  "index": "apps/spa/src/index.html",
  "main": "apps/spa/src/main.ts",
  "polyfills": "apps/spa/src/polyfills.ts",
  "tsConfig": "apps/spa/tsconfig.app.json",
  "assets": [
    "apps/spa/src/favicon.ico",
    "apps/spa/src/assets",
    "apps/spa/src/mockServiceWorker.js"
  ],
  "allowedCommonJsDependencies": [
    "debug"
  ],

Now let's create our first mock route. Examining the app.component, we can see:

@Component({
  selector: 'msw-tutorial-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  hello$ = this.http.get<Message>('/api/hello');
  constructor(private http: HttpClient) {}
}

Two things stand out: first, making an HTTP call directly in the component is a questionable pattern; second, the call targets /api/hello.

Time to add a matching handler:

File: handlers.ts

import { rest } from 'msw';

export const handlers = [
  rest.get('/api/hello', async (req, res, ctx) => {
    return res(ctx.json({ message: 'Msw works like a charm!' }));
  }),
];

The syntax will feel familiar—it's very much like working with express.

Reloading the page, we now see:

Main application with mocking working

Our mock is live!

The console also confirms the interception:

Console showing MSW details

This is exactly what we wanted.

To verify the behavior, restart the app in regular development mode:

$ npx nx serve

This time, there's no trace of the mock in the console—the app runs against the real backend as expected.

Wrapping Up

MSW provides a simple yet powerful layer for mocking network requests. You can choose to intercept the entire API or selectively mock only specific endpoints.

Once configured, adding new mocks is just a matter of including more handlers. These can be as simple or as elaborate as your testing needs demand—from static JSON fixtures to generated data with libraries like faker.

The real strength lies in its invisibility to Angular. There's no service replacement, no special wiring, and importantly, no risk of accidentally shipping mock behavior to production.

Even e2e tests benefit without any extra effort. Since they run against a live instance of the app, launching it with the mock configuration means your end-to-end suite automatically uses the mocked data.

Unit tests can also leverage the mocks, though I'd argue they're better off avoiding HTTP entirely, whether real or mocked.

You can find the complete working example on github.