Next.jsのApp routerでMSWをserver componentsとclient componentsで利用する方法

こんにちは、サイオステクノロジーの遠藤です。

前回はReact Router v7でMSWを利用する方法を整理しました。

今回はNext.jsのApp routerでMSW(Mock Service Worker)を利用する方法について整理していきます。

はじめに

今回の内容は以下のDraft状態のmsw exampleのPull requestを参考に作成しています。

https://github.com/mswjs/examples/pull/101

記事執筆を行っている2025/03の段階ではDraftかつ、Next.js側の問題でブラウザ側でのHMR(Hot Module Replacement) の問題があることが以下のissueで言及されています。

https://github.com/mswjs/msw/issues/1644#issuecomment-2433234922

時間が経てば状況が変わることが予想されますので、そのときには本記事は参考程度にしていただき、上記のPull requestやissueの内容を確認していただければと思います。

MSWの準備を行う

MSWを使用するために各種準備を行っていきます。Next.jsアプリは用意済みという前提で進めていきます。

MSWをインストールする

最初にMSWのインストールを行います。

npm install msw -D

mockServiceWorker.jsをコピー

MSWでは、worker scriptをアプリケーションのpulbicディレクトリに配置することでクライアント上でリクエストをキャッチできるようになります。こちらは以下の形でMSWのCLIを利用することで自分のディレクトリにworkerscriptである mockServiceWorker.jsがpublic直下に作成されます。

npx msw init ./public --save

統合モジュールを用意する

server componentsとclient componentsで利用する統合モジュールを用意していきます。これらのファイルを配置するディレクトリとしてプロジェクトのrootディレクトリにmocksというディレクトリを用意してその中にファイルを配置していきます。

MSWのhandlersを定義する

mockの内容となるMSWのhandlersをmocks/handlers.tsに定義します。

// mocks/handlers.ts

import { graphql, http, HttpResponse } from "msw";

export type User = {
  firstName: string;
  lastName: string;
};

export type Movie = {
  id: string;
  title: string;
};

export const handlers = [
  http.get<never, never, User>("<https://api.example.com/user>", () => {
    return HttpResponse.json({
      firstName: "Sarah",
      lastName: "Maverick",
    });
  }),
  graphql.query<{ movies: Array<Movie> }>("ListMovies", () => {
    return HttpResponse.json({
      data: {
        movies: [
          {
            id: "6c6dba95-e027-4fe2-acab-e8c155a7f0ff",
            title: "123 Lord of The Rings",
          },
          {
            id: "a2ae7712-75a7-47bb-82a9-8ed668e00fe3",
            title: "The Matrix",
          },
          {
            id: "916fa462-3903-4656-9e76-3f182b37c56f",
            title: "Star Wars: The Empire Strikes Back",
          },
        ],
      },
    });
  }),
];


browser用の統合モジュールを作成する

browser用(client component用)の統合モジュールをmocks/browser.tsに定義します。

// mocks/browser.ts

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

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

node.js用の統合モジュールを作成する

node.js用(server component用)の統合モジュールをmocks/node.jsに定義します。

// mocks/node.ts

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

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


App routerでMSWを使用する

ここからはapp以下で作業を進めていきます。

MSW用のProviderを作成する

layout.tsxでMSW用のProviderをapp/mswProvider.tsxに定義します。このProviderで囲うことで、このProvider内のclient componentについてMSWを利用することが出来るようになります。

// app/mswProvider.tsx

"use client";

import { handlers } from "@/mocks/handlers";
import { Suspense, use } from "react";

const mockingEnabledPromise =
  typeof window !== "undefined" && process.env.NODE_ENV === "development"
    ? import("@/mocks/browser").then(async ({ worker }) => {
        await worker.start({
          onUnhandledRequest(request, print) {
            if (request.url.includes("_next")) {
              return;
            }
            print.warning();
          },
        });
        worker.start();

        console.log(worker.listHandlers());
      })
    : Promise.resolve();

export function MSWProvider({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  // If MSW is enabled, we need to wait for the worker to start,
  // so we wrap the children in a Suspense boundary until it's ready.
  return (
    <Suspense fallback={null}>
      <MSWProviderWrapper>{children}</MSWProviderWrapper>
    </Suspense>
  );
}

function MSWProviderWrapper({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  use(mockingEnabledPromise);
  return children;
}

layout.tsxでnode.js用の統合モジュールの呼び出しとmsw用のProviderでchildrenをラップする

続いてlayout.tsxにてserver側(Runtimeがnode.js)でMSWが利用できるようにします。また、先ほど作成したmsw用のProviderでchildrenをラップします。ここまで終われば準備完了です!

// app/layout.tsx

import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { MSWProvider } from "./mswProvider";

if (
  process.env.NEXT_RUNTIME === "nodejs" &&
  process.env.NODE_ENV === "development"
) {
  const { server } = await import("@/mocks/node");
  server.listen();
}

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <MSWProvider>{children}</MSWProvider>
      </body>
    </html>
  );
}


server component上でfetchを行ってみる

ではまずはserver component上でfetchを行ってみましょう。app/page.tsxでhandlerで定義したapiを呼び出してみましょう。

// app/page.tsx

import { User } from "@/mocks/handlers";

async function getUser() {
  const response = await fetch("<https://api.example.com/user>");
  const user = (await response.json()) as User;
  return user;
}

export default async function Home() {
  const user = await getUser();

  return (
    <main>
      <p id="server-side-greeting">Hello, {user.firstName}!</p>
    </main>
  );
}


開発モードで起動して動作を確認してみると、handlerで定義したレスポンスが取れていることが確認できました!

client component上でfetchを行ってみる

続いてclient component上でfetchを行ってみます。client componentとして実行されるように”use client”をつけてapp/movieList.tsxを作成します。

// app/movieList.tsx

"use client";

import { Movie } from "@/mocks/handlers";
import { useState } from "react";

export function MovieList() {
  const [movies, setMovies] = useState<Array<Movie>>([]);

  const fetchMovies = () => {
    fetch("/graphql", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        query: `
          query ListMovies {
            movies {
              id
              title
            }
          }
        `,
      }),
    })
      .then((response) => response.json())
      .then((response) => {
        setMovies(response.data.movies);
      })
      .catch(() => setMovies([]));
  };

  return (
    <div>
      <button id="fetch-movies-button" onClick={fetchMovies}>
        Fetch movies
      </button>
      {movies.length > 0 ? (
        <ul id="movies-list">
          {movies.map((movie) => (
            <li key={movie.id}>{movie.title}</li>
          ))}
        </ul>
      ) : null}
    </div>
  );
}


page.tsxでmovieList.tsxを呼び出す

そしたら作成したmovieList.tsxをpage.tsxに加えて動作を確認してみましょう。

// app/page.tsx

import { User } from "@/mocks/handlers";
import { MovieList } from "./movieList";

async function getUser() {
  const response = await fetch("<https://api.example.com/user>");
  const user = (await response.json()) as User;
  return user;
}

export default async function Home() {
  const user = await getUser();

  return (
    <main>
      <p id="server-side-greeting">Hello, {user.firstName}!</p>
      <MovieList />
    </main>
  );
}

ページを表示して 「Fetch movies」をクリックするとしっかりとclient component上でもfetch出来ていることを確認できました!

まとめ

今回はNext.jsのApp routerでMSWを利用する方法についてまとめました。「はじめに」にも書きましたが、今回紹介した内容はDraftのPull requestを参考にさせていただいたものになっているので、本記事に関しては参考程度にしていただくのが良いかと思います。

ではまた~

参考にさせていただいた記事

https://github.com/mswjs/msw/issues/1644

https://qiita.com/tarosuke777000/items/622e5ce3e3ace102560a

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

0人がこの投稿は役に立ったと言っています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です