MSW in SvelteKit

How to implement MSW for local development in SvelteKit

Using MSW in SvelteKit

This is a tutorial that will show you how to implement the “Mock Service Worker” library, caleld MSW, in your SvelteKit application. Please note that this guide doesn’t show how to set up MSW with Jest, but rather how to use MSW during live development.

The complete implementation is available in the repository of spikze.club, link also in the addendum at the end of the page.

Don’t worry if this guide might look overwhelming at first, the changes are quite simple. Using MSW in SvelteKit to mock requests during local development just requires a little more effort.

Installing dependencies

First, let’s install all necessary dependencies.

npm i -D msw ts-node concurrently

We’re using “ts-node” to also start the server-mocks. Before we proceed, please call the following command in the root of your SvelteKit-project.

npx msw init ./static

This will generate a Javascript-file with the default service worker handler by MSW. We’re using “static” for our SvelteKit-app, as it’s the equivalent to the public directory. Finally, let’s update the package.json-file so that MSW knows where to look for the file.

{
  "scripts": ...
  ...
  "msw": {
    "workerDirectory": "static"
  }
}

As a convenient script, I also added the following command to my “scripts”-configuration which is needed if you want to start MSW both via client as well as server (for server-side mocks).

{
  "scripts": {
    "dev": "svelte-kit dev",
    "dev:msw-server": "concurrently \"cd msw && ts-node server.ts\" \"npm run dev\"",
    ...
  },
}

Preparing MSW-modules

Before we start writing actual code, we need to define our new working directory for MSW in SvelteKit’s config as well as Typescript-config. We'll start with the Typescript-file.

{
  "compilerOptions": {
    ...,
    "paths": {
      "$lib": ["src/lib"],
      "$lib/*": ["src/lib/*"],
      "$msw": ["src/msw"],
      "$msw/*": ["src/msw/*"]
    }
  }
}

Next, let's update the SvelteKit-config.

import adapter from "@sveltejs/adapter-auto";
import preprocess from "svelte-preprocess";
import path from "path";

/** @type {import('@sveltejs/kit').Config} */
const config = {
  // Consult https://github.com/sveltejs/svelte-preprocess
  // for more information about preprocessors
  preprocess: preprocess(),
  kit: {
    adapter: adapter(),
    vite: {
      ...,
      resolve: {
        alias: {
          $msw: path.resolve("./src/msw"),
          $lib: path.resolve("./src/lib")
        }
      }
    }
  }

Adding the mocks and handlers

Alright, you’ve made it through the first part of the guide! Now we’ll add the actual code that gets executed when using MSW. Please note that this is a minimal example, therefore I added what’s necessary to make it all work, but no more.

As you might have guessed from our changes to the SvelteKit-config, all code will be placed in a new directory called “msw”, which lives directly in “src”.

|- app/
|-- src/
|--- pages/
|--- msw/
|---- fixtures/
...

Here’s the code for the next modules to add. You should be able to simply copy-paste them, the file name + directory path is written down as a comment in each block at the top.

//
// app/src/msw/handlers.server.ts
//

import { rest } from "msw";
import { values } from "./fixtures/msw-demo";

export const handlers = [
  // Here, you can mock absolute URL requests,
  // e.g. to a database. For the current implementation,
  // no data is mocked in this place.
  //
  // Note: This is also the place to mock absolute
  // SSR-imports. Everything in 'handlers.workers.ts'
  // is mocked client-side.
];
//
// app/src/msw/handlers.worker.ts
//

import { rest } from "msw";
import { values } from "./fixtures/msw-demo";

// Mock relative URLs that map to your
// routes' data endpoints. This mock only
// happens for client-side requests.
//
// Note that if you use shadow endpoints, this still works
// as the endpoint gets created by SvelteKit.
export const handlers = [
  rest.get("/msw/demo/__data.json", (req, res, ctx) => {
    return res(ctx.status(200), ctx.json({ values }));
  })
];
//
// app/src/msw/server.ts
//

import { setupServer } from "msw/node";
import { handlers } from "./handlers.server";

export const server = setupServer(...handlers);
//
// app/src/msw/worker.ts 
//

import { setupWorker } from "msw";
import { handlers } from "./handlers.worker";

export const worker = setupWorker(...handlers);
//
// app/src/msw/fixtures/msw-demo.ts
//

export const values = ["foo", "bar"];
//
// app/src/msw/index.ts 
//

import { browser, dev } from "$app/env";

/**
 * Lazy-inject the MSW handler
 * so that no errors happen during
 * build/runtime due to invalid
 * imports from server/client.
 */
export async function inject() {
  if (dev && browser) {
    const { worker } = await import("../msw/worker");
    // For live development, I disabled all warnings
    // for requests that are not mocked. Change how
    // you think it best fits your project.
    return worker.start({ onUnhandledRequest: "bypass" }).catch(console.warn);
  }
  if (dev && !browser) {
    const { server } = await import("../msw/server");
    // Same as in worker-mock above.
    return server.listen({ onUnhandledRequest: "bypass" });
  }
}

Starting MSW

One challenge when using MSW during live development is that we have to make sure there’s no race condition. We have to define an order of execution, else the service worker from MSW might become active after all requests are already made.

To accomplish this goal, we modify our root layout-file. As this file is mounted for every page, it’s a good place to block all further execution until MSW has finished.

<script>
  import "../app.css";
  import { dev } from "$app/env";
  
  // Loaded from .env.local, guide covers this
  // step in a moment.
  const isMswEnabled = dev && import.meta.env.VITE_MSW_ENABLED === "true";
  // Flag to defer rendering of components
  // until certain criteria are met on dev,
  // e.g. MSW init.
  let isReady = !isMswEnabled;
  
  if (isMswEnabled) {
    import("$msw")
      .then((res) => res.inject())
      .then(() => (isReady = true));
  }
</script>

{#if isReady}
  <slot />
{/if}

Adding demo page

The following code snippets simply show contents for two demo pages and the one endpoint used in this tutorial.

<!-- app/src/routes/msw/index.svelte -->

<script>
  import DisplayProse from "$lib/display/views/DisplayProse.svelte";
  import ProminentDisplayTitle from "$lib/display/views/ProminentDisplayTitle.svelte";
  import PageLayout from "$lib/layout/views/PageLayout.svelte";
  import SectionLayout from "$lib/layout/views/SectionLayout.svelte";
</script>

<PageLayout>
  <SectionLayout withContentTopSpacing withHeaderSpacing>
    <ProminentDisplayTitle slot="header" color="primary">MSW Landing Page</ProminentDisplayTitle>
    <DisplayProse>
      <p>
        This compoonent has no purpose other than being part of an MSW demo implementation. See <a
          href="https://flaming.codes"
          alt="Link to flaming.codes with blog posts">flaming.codes</a
        > for more details.
      </p>
      <p>
        This page doesn't fetch any data for shows how client-side fetches are mocked with MSW in
        SvelteKit.
      </p>
      <p>Simply click the link below to access the page with data.</p>
      <p>
        <a href="/msw/demo" alt="Link to demo page with data">msw/demo</a>
      </p>
    </DisplayProse>
  </SectionLayout>
</PageLayout>
<!-- app/src/routes/msw/demo.svelte -->

<script lang="ts">
  import ProminentDisplayTitle from "$lib/display/views/ProminentDisplayTitle.svelte";
  import PageLayout from "$lib/layout/views/PageLayout.svelte";
  import SectionLayout from "$lib/layout/views/SectionLayout.svelte";
  export let values: string[];
</script>

<PageLayout>
  <SectionLayout withHeaderSpacing withContentTopSpacing>
    <ProminentDisplayTitle slot="header" color="primary">MSW Demo</ProminentDisplayTitle>
    <p>
      This compoonent has no purpose other than being part of an MSW demo implementation. See <a
        href="https://flaming.codes"
        alt="Link to flaming.codes with blog posts">flaming.codes</a
      > for more details.
    </p>
    <p>
      Values: {values}
    </p>
  </SectionLayout>
</PageLayout>
//
// app/src/routes/msw/demo.ts
//

import type { RequestHandler } from "@sveltejs/kit";

// Just for demo purposes.
export const get: RequestHandler = async () => ({
  status: 200,
  body: {
    values: ["production", "data", "not", "msw"]
  }
});

Adding an environment variable

We’re almost done. What’s missing is to add the “VITE_MSW_ENABLED”-flag to our environment. We’re using “.env.local” as our file to hold the flag, as this will be consumed by Vite and not added to git.

VITE_MSW_ENABLED=true

Running the application

Alright, everything should be ready to use now! To enable mocking on the client, simply ensure the flag is set. Run the common “dev”-command then to mock client-side requests.

npm run dev

To also mock server-side requests, simply run the new command we added at the start.

npm run dev:msw-server

Now you’re ready to mock endpoints during local development. As noted in the code example, this demo page doesn't include server-side mocks, albeit everything is prepared for it. This means that locally, the MSW-call only gets triggered if you navigate from the index-page of the demo site to the actual demo-page via a click on the link.

To simulate server-requests, you'll have more complicated endpoints that also fetch data from databases. Those requests can then be mocked in the server-handler. Note that for the server-mocks, only absolute URLs are valid.

Suggestions

Related

Addendum

Languages