Skip to main content
  1. Posts/

Setting up MSW in Expo 55 is finally boring

·8 mins

MSW is one of those tools that always made sense on paper.

Mock the network, keep your components clean, simulate success and failure states without rewriting your app around mocks. Great.

But in React Native, and especially in older Expo setups, the experience was not always that smooth.

There was usually some amount of “yes, it works, but…” around missing web APIs, polyfills, import order, or runtime differences that made the setup feel more fragile than it should.

With Expo SDK 55, that story is much better.

This post expands on the same idea I recently pointed out in an MSW docs discussion: setting up MSW in Expo is now much closer to normal. And that is a big deal, because API mocking should feel boring.

First, what is MSW in React Native? #

MSW stands for Mock Service Worker.

On the web, people often associate it with Service Workers. That name can be a bit misleading when you move to React Native, because native apps do not use a browser Service Worker.

So what happens here?

In React Native, MSW uses native-compatible request interception through msw/native.

The mental model is still the same:

  • define request handlers
  • intercept outgoing requests
  • return mocked responses

What changes is the runtime strategy, not the value of the tool.

Why should you care? #

Because MSW solves a real testing problem.

Not a theoretical one. A very practical one.

Without MSW, teams often end up doing one of these things:

  • hardcoding fake data inside components
  • mocking API functions directly in the app just to unblock the UI
  • adding ad-hoc if (__DEV__) branches all over the app
  • waiting for backend endpoints before building the screen
  • creating tiny fake clients that slowly diverge from reality

That gets messy fast.

MSW gives you one dedicated layer for network mocking.

That means:

  • your app still calls real URLs
  • your UI stays close to production behavior
  • success, empty, loading, and error states become easy to reproduce
  • frontend can move without being blocked by backend readiness

In simple terms, it lets you mock the API without turning the app itself into a mock.

Why Expo 55 makes this easier #

This is the part that matters most.

Expo’s current runtime is much closer to standard web APIs than older React Native setups used to be.

According to Expo’s documentation, expo/fetch now provides a WinterCG-compliant Fetch API that works consistently across web and mobile. Expo’s docs also say the built-in URL and URLSearchParams implementations replace the older shims in React Native, and that the standard URL API is available on all platforms. Expo also documents that TextEncoder is included in Hermes, and Expo uses Hermes by default.

Why does this matter for MSW?

Because tools like MSW work best when the runtime behaves more like the platform they expect.

My inference from those Expo runtime improvements is simple: a big part of the old MSW-in-Expo pain was not MSW itself. It was the environment mismatch around network and URL-related APIs.

That mismatch is smaller now.

And when the mismatch gets smaller, the setup gets less weird.

What used to feel annoying before Expo 55 #

If you configured MSW in older Expo or React Native setups, you probably ran into some mix of these problems:

  • extra polyfills
  • runtime errors around request handling
  • setup steps that felt unusually sensitive to import order
  • documentation that technically worked but did not feel stable enough

A very common example was needing a file like this before even getting to the handlers:

// msw.polyfills.js
import "fast-text-encoding";
import "react-native-url-polyfill/auto";

Why those two?

  • fast-text-encoding was there to polyfill TextEncoder
  • react-native-url-polyfill/auto was there to polyfill URL and related APIs

That used to be one of the annoying parts of the setup. Before even talking about handlers, you first had to make the runtime look more like the web.

With Expo 55, that story is much better.

According to Expo’s docs, TextEncoder is already included in Hermes. Expo’s docs also say URL and URLSearchParams are built in on native platforms, and that those implementations replace the old React Native shims.

So if you are using Expo 55, you are also using Hermes by default, which means those two polyfills are no longer the first thing you need to install just to get MSW off the ground.

That is a big part of why this setup now feels easier.

That was the frustrating part.

MSW is supposed to reduce friction, not introduce a new category of it.

Expo 55 does not magically remove every edge case in every library stack, but it does make the baseline much healthier.

That changes MSW from “interesting, but maybe later” into “yes, this is a reasonable default for test-time API mocking”.

What I like about MSW specifically #

There are a lot of ways to fake API data.

Most of them age badly.

What I like about MSW is that it keeps the mocking logic close to the network boundary instead of pushing it inside the UI.

That gives you a few concrete benefits:

1. Your screens stay honest #

Your screen still performs the request.

You are not bypassing the data layer just to make the UI render.

That usually gives you better confidence than injecting random mock props directly into components.

2. It helps you test against an expected contract #

You can define the response shape early and mock errors deliberately without depending on a live backend.

That is especially useful when you want deterministic tests around UI states.

3. It makes ugly states easy to reproduce #

Success states are easy.

What usually hurts are:

  • empty responses
  • validation errors
  • slow endpoints
  • partial data
  • server failures

MSW makes those states easy to simulate on purpose.

4. It is a better long-term habit than scattering fake data everywhere #

This is probably my most opinionated point.

I think teams underestimate how much accidental architecture damage comes from temporary fake data.

MSW gives that temporary layer a proper home.

That is cleaner, easier to remove, and easier to reason about.

A practical setup in Expo 55 #

Let’s keep it simple.

Install MSW:

npm install msw

Now create a small mocks folder.

1. Define your handlers #

// src/mocks/handlers.ts
import { http, HttpResponse } from "msw";

const API_URL = "https://api.example.com";

export const handlers = [
  http.get(`${API_URL}/profile`, () => {
    return HttpResponse.json({
      id: "user-1",
      name: "Marco",
      role: "admin",
    });
  }),

  http.get(`${API_URL}/notifications`, () => {
    return HttpResponse.json([
      { id: "1", title: "Welcome", read: false },
      { id: "2", title: "Build finished", read: true },
    ]);
  }),
];

This is the core idea.

You describe the network behavior you want. Not component hacks. Not fake hooks. Network behavior.

2. Create the native server #

// src/mocks/server.ts
import { setupServer } from "msw/native";

import { handlers } from "./handlers";

export const server = setupServer(...handlers);

3. Start it from jest.setup.ts #

This is the part I would keep simple.

Instead of adding bootstrap logic in the app runtime, put the MSW lifecycle in jest.setup.ts. That keeps it where it belongs for tests and avoids adding app startup behavior just to support mocking.

// jest.setup.ts
import { server } from "./src/mocks/server";

beforeAll(() => {
  server.listen({
    onUnhandledRequest: "bypass",
  });
});

afterEach(() => {
  server.resetHandlers();
});

afterAll(() => {
  server.close();
});

And in your Jest config:

// jest.config.js
module.exports = {
  preset: "react-native",
  setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
};

That is the nice part.

The setup stays small, and the handlers are still the main story.

A small example screen #

Here is a simple component that consumes the mocked endpoint:

// src/app/profile.tsx
import { useEffect, useState } from "react";
import { Text, View } from "react-native";

type Profile = {
  id: string;
  name: string;
  role: string;
};

export default function ProfileScreen() {
  const [profile, setProfile] = useState<Profile | null>(null);

  useEffect(() => {
    fetch("https://api.example.com/profile")
      .then((response) => response.json())
      .then((data: Profile) => {
        setProfile(data);
      });
  }, []);

  if (!profile) {
    return <Text>Loading...</Text>;
  }

  return (
    <View>
      <Text>{profile.name}</Text>
      <Text>{profile.role}</Text>
    </View>
  );
}

Nothing in that screen knows it is talking to a mock.

That is exactly the point.

Trade-offs and limits #

MSW is not magic, and it is not a replacement for everything.

What it does well:

  • test-time API mocking
  • deterministic UI states
  • keeping mocks outside the component tree
  • reusing request handlers across environments

What it does not replace:

  • real backend integration testing
  • contract validation by itself
  • end-to-end confidence against a real deployed backend

Also, one warning that matters:

If your mocks drift too far from the real API contract, you will create false confidence. That is not an MSW problem. That is a discipline problem.

My opinionated take #

I think MSW is one of the best answers to a very common frontend mess: pretending that fake data inside the UI is “good enough for now”.

Usually it is not.

That approach spreads quickly, gets inconsistent, and then nobody trusts the states they are seeing anymore.

MSW fixes that by putting the mock where it belongs: at the network boundary.

And Expo 55 makes this much easier to justify because the setup no longer feels like a side quest.

Conclusions #

Configuring MSW in Expo used to feel more fragile than it should.

With Expo 55, the setup is much more normal, and that matters because tools like this only become defaults when they stop fighting the runtime.

If your team builds API-heavy screens in Expo, I think MSW is now one of the cleanest ways to mock network behavior in tests without polluting the app code.