20251027

2025/10/27

MSW

https://mswjs.io/

Mock Service Workerの略。

ブラウザの Service Worker 機能を使ったAPIモックツール。ブラウザの fetch を横取りしてテスト用のデータを返す。

Service Worker が無い Node.js では http.ClientRequest 系のリクエストをインターセプトするらしい。これによって、RSCなどサーバーからのリクエストを行うケースでもリクエストを横取りできる。

net モジュールを直接使うリクエスト(undici など)のインターセプトはできないらしい。

https://mswjs.io/docs/limitations/

インストールの概要

仕組み上、別でAPIサーバーを用意するのではなく、アプリケーションに Service Worker や Node.js 用の処理を組み込むことになる。

  • Service Worker 用の設定(ブラウザ側のfetch)
    • Service Worker スクリプトの設置
      • npx msw init <PUBLIC_DIR> --save で自動生成できる
    • Service Worker を登録するためのスクリプト追加
  • Node.js 用の設定(サーバー側のfetch)
    • サーバーのセットアップ処理追加
  • リクエストハンドラの設定
    • 特定のリクエストに対するレスポンスをモック化する処理の追加

Next.js での使用の注意点

Next.js ではサーバー側、クライアント側ともに諸々隠蔽されているのでさらに設定が必要。

サーバー側では Node.js の挙動をいじるために instrumentation という機能を使用する。

https://nextjs.org/docs/15/app/guides/instrumentation

instrumentation.ts は サーバープロセスが起動するときに一度だけ呼び出される。MSWを使用する際はここでサーバーの挙動を上書きする。

クライアント側では初回のレンダリング前に ServiceWorker の設定を行いたいため、 コンテンツの親コンポーネントを新たに追加し、ServiceWorkerの準備が整ってからコンテンツを表示させる仕組みにする。

※ instrumentation-client.ts でもいいがうまく動かないケースがあるとのこと

https://dev.to/ajth-in/mock-client-side-server-side-api-requests-using-nextjs-and-mswjs-9f1

リクエストハンドラの設定

// lib/msw/handlers/index.ts

import { http, HttpResponse } from "msw";

export const handlers = [
  http.get("APIのURL", () => {
    return HttpResponse.json({
      message: "Hello, world!",
    });
  }),
];

モックしたいURLとモックデータを記述する。これを後で MSW の setupWorkersetupServer に渡す。

Node.js 用の設定(サーバー側の設定)

// lib/msw/setup/server.ts

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

export const server = setupServer(...handlers);
// instrumentation.ts

export async function register() {
  if (process.env.NEXT_RUNTIME === "nodejs") {
    const { server } = await import("./lib/msw/setup/server");
    server.listen();
  }
}

先ほど作成したリクエストハンドラーを setupServer に渡してサーバー設定を構築し、 instrumentation.ts でサーバーに適用する。

server.listen() でサーバーを起動しているように見えるが、実際はすでに起動している開発サーバーに対してリクエストを横取りするパッチを当てている。

Service Worker 用の設定(クライアント側の設定)

以下コマンドで自動的に Service Worker のスクリプトが生成される。 <PUBLIC_DIR> で指定した場所に設置されるため、ドキュメントルートで問題ない。

npx msw init <PUBLIC_DIR> --save

このファイルはそのまま使えるので手を加える必要はない。

// lib/msw/setup/browser.ts

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

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

こちらはリクエストハンドラーを setupWorker に渡して ServiceWorker との連携設定を構築。(仕組みとしては ServiceWorkerで横取り → クライアントにモックデータを確認 → ServiceWorkerからモックデータをレスポンス、と言う流れっぽい?)

設定の反映を初回レンダリング前に行う必要があるため、それ用のコンポーネントを作成する。

// lib/msw/provider/index.tsx

"use client";

import { ReactNode, useEffect, useState } from "react"

export default function MswProvider({children}: {children: ReactNode}) {
  const [mockingEnabled, enableMocking] = useState(false);

  useEffect(() => {
    async function mock() {
      const { worker } = await import('../setup/browser');
      await worker.start();
      enableMocking(true);
    }

    mock();
  }, []);

  if (!mockingEnabled) {
    return null;
  }

  return <>{children}</>
}

ServiceWorkerの諸々が終わってから子コンポーネントが表示されるようにしている。

// app/layout.tsx

import { ReactNode } from "react";
import MswProvider from '@/lib/msw/provider';

export default function RootLayout({children}: {children: ReactNode}) {
  return (
    <html lang="ja">
      <body>
        <MswProvider>
          { children }
        </MswProvider>
      </body>
    </html>
  )
}

layout.tsx にて先ほどのコンポーネントでコンテンツ部分をラップ。

本番環境用の設定

サーバー設定やServiceWorker用の設定は本番環境では除外したいのでその対応も必要になる。

調べきれてないのでまた調べる。