0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MSWで実現した『BEを待たない』開発。導入・実装ドキュメントのすべて

Last updated at Posted at 2026-01-30

BEを待つな_自律型フロントエンド開発_page-0002.jpg

はじめに

本ドキュメントは、MSW(Mock Service Worker)の導入・実装ドキュメントです。
基本概念から実装方法まで解説しています。

プロジェクトへの新規導入 を想定して、ゼロから構築できるよう順を追って説明します。

MSW公式ドキュメントは以下です。
https://mswjs.io/

1. MSWの概要

MSWとは

MSW(Mock Service Worker)とは、
ブラウザのService Worker APIを利用してネットワークリクエストをインターセプト(横取り)し、任意のレスポンスを返すモックツール 」です。

従来のAPIモック手法では、アプリケーションコード内に条件分岐を入れる、またはモック用のアダプターを挟む必要がありました。

MSWを使用すると、アプリケーションコードを変更せずにAPIの振る舞いを差し替えることができます。

対応する開発課題

フロントエンド開発では往々にして、
API仕様は決まっているが、バックエンドの実装が完了していないため、フロントエンド開発を進められない
といった場合があります。

また、エラー発生時のUI確認や大量データでの動作検証をする際、バックエンド側でそのような状態を作り出す作業が発生します。
ローカル開発時にバックエンドサーバーへの接続が不安定な場合や、VPN接続が必要な場合もあります。

MSWを導入することで、バックエンドの状態に依存せずにフロントエンド開発を進めることができます。

MSWの動作原理

MSWの仕組みを理解するために、通常のAPI通信とMSW有効時の通信を比較します。

通常のAPI通信フロー

アプリケーションがfetchaxiosでAPIを呼び出すと、リクエストはブラウザを経由してバックエンドサーバーに到達し、サーバーからのレスポンスがアプリケーションに返されます。

MSW有効時のフロー

MSWが有効な場合、Service Workerがリクエストを横取りします。
事前に定義しておいたハンドラーがリクエストを処理し、モックデータをレスポンスとして返します。
このとき、バックエンドサーバーにはリクエストが届きません。

アプリケーションから見ると、本物のAPIからレスポンスが返ってきたように動作します。

バックエンド完成後も、fetchを呼び出すアプリケーションコードについて、変更する必要はありません。

Service Workerについて

Service Workerは、ブラウザがWebページとは別のスレッドで実行するスクリプトです。
オフライン対応やプッシュ通知などに使用されますが、MSWはこの機能を活用してネットワークリクエストをインターセプトしています。

Service Worker は登録時にスコープを持ち、スコープ配下のページから発生するリクエストをインターセプトできます。

mockServiceWorker.js の配置(登録URL)を変えると、制御できる範囲も変わるため、通常は ルート配下で登録するのが安全です。

MSWはこの仕組みを利用して、開発者が定義したルールに基づいてリクエストを処理します。


2. MSWの導入手順

ここからは、プロジェクトにMSWを導入する手順を解説します。
Vite + React + TypeScriptの環境を前提としますが、他の環境でも基本的な流れは同様です。

パッケージのインストール

MSWをプロジェクトにインストールします。
開発時のみ使用するため、--save-devオプションを付けます。

npm install msw --save-dev

Service Workerファイルの生成

MSWが動作するためには、Service Workerのスクリプトファイルが必要です。
以下のコマンドで生成します。

npx msw init public/ --save

このコマンドを実行すると、public/mockServiceWorker.jsが生成されます。
このファイルはMSWが提供するもので、中身を編集する必要はありません。

--saveオプションを付けると、package.jsonに以下の設定が追加されます。

package.json(通常はこれ)

{
  "msw": {
    "workerDirectory": "./public"
  }
}

monorepo等(複数ディレクトリが必要な場合)

{
  "msw": {
    "workerDirectory": [
      "./packages/app-one/public",
      "./packages/app-two/public"
    ]
  }
}

これにより、MSWのバージョンアップ時にService Workerファイルも更新されます。

ディレクトリ構成

MSW関連のファイルを配置するディレクトリ構成の例を示します。

src/
├── mocks/
│   ├── browser.ts          # ブラウザ用MSWセットアップ
│   ├── enableMocking.ts    # MSW有効化の制御
│   ├── handlers/
│   │   ├── index.ts        # ハンドラー統合
│   │   ├── userHandlers.ts # ユーザー関連API
│   │   └── postHandlers.ts # 投稿関連API
│   └── data/
│       └── userData.ts     # モック用テストデータ
├── components/
│   └── MswIndicator.tsx    # MSW有効表示コンポーネント
├── main.tsx
└── ...

public/
└── mockServiceWorker.js    # 生成されたService Worker

機能ごとにハンドラーファイルを分けることで、ファイルの肥大化を防ぐことができます。

ハンドラーの作成

ハンドラーは「このURLにリクエストが来たら、このレスポンスを返す」というルールを定義したものです。

個別のハンドラーファイルを作成します。

// src/mocks/handlers/userHandlers.ts
import { http, HttpResponse, delay } from 'msw';

// レスポンス遅延時間(ミリ秒)
// 実際のAPI呼び出しに近い動作を再現するため、遅延を入れる
const MOCK_DELAY_MS = 100;

export const userHandlers = [
  // ユーザー一覧取得
  http.get('/api/users', async () => {
    await delay(MOCK_DELAY_MS);

    return HttpResponse.json([
      { id: 1, name: '田中太郎', email: 'tanaka@example.com' },
      { id: 2, name: '鈴木花子', email: 'suzuki@example.com' },
    ]);
  }),

  // 特定ユーザー取得(URLパラメータを使用)
  http.get('/api/users/:userId', async ({ params }) => {
    await delay(MOCK_DELAY_MS);

    const { userId } = params;

    return HttpResponse.json({
      id: Number(userId),
      name: `ユーザー${userId}`,
      email: `user${userId}@example.com`,
    });
  }),

  // ユーザー作成
  http.post('/api/users', async ({ request }) => {
    await delay(MOCK_DELAY_MS);

    // リクエストボディを取得
    const body = await request.json();

    // 作成されたユーザーを返す
    return HttpResponse.json(
      { id: Date.now(), ...body },
      { status: 201 }
    );
  }),
];

すべてのハンドラーを統合するファイルを作成します。

// src/mocks/handlers/index.ts
import { userHandlers } from './userHandlers';
// 他のハンドラーがあればここでimport
// import { postHandlers } from './postHandlers';

// 全ハンドラーを統合してexport
export const handlers = [
  ...userHandlers,
  // ...postHandlers,
];

ブラウザ用セットアップ

MSWをブラウザで動作させるためのセットアップファイルを作成します。

// src/mocks/browser.ts
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';

// Service Workerを設定し、ハンドラーを登録
export const worker = setupWorker(...handlers);

MSW有効化の制御

環境変数に応じてMSWを有効化するかどうかを制御するファイルを作成します。

// src/mocks/enableMocking.ts

export async function enableMocking(): Promise<void> {
  // 本番環境では何もしない
  if (import.meta.env.PROD) {
    return;
  }

  // 環境変数でMSWが有効化されていない場合は何もしない
  if (import.meta.env.VITE_MSW_ENABLED !== 'true') {
    return;
  }

  // MSWのworkerを動的にimport(コード分割のため)
  const { worker } = await import('./browser');

  // Service Workerを起動
  await worker.start({
    // ハンドラーが定義されていないリクエストの扱い
    // 'bypass': そのまま実際のサーバーに送る
    // 'warn': コンソールに警告を出しつつ、実際のサーバーに送る
    // 'error': エラーを投げる
    onUnhandledRequest: 'bypass',
  });

  console.log('[MSW] Mocking enabled.');
}

アプリケーションへの組み込み

アプリケーションのエントリーポイントで、MSWの初期化を行います。
MSWの起動は非同期処理のため、起動完了を待ってからReactアプリをマウントします。

// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { enableMocking } from './mocks/enableMocking';

// MSWの初期化を待ってからアプリをマウント
enableMocking().then(() => {
  ReactDOM.createRoot(document.getElementById('root')!).render(
    <React.StrictMode>
      <App />
    </React.StrictMode>
  );
});

この実装により、MSWが有効な環境ではService Workerの起動を待ってからアプリが動き始めます。
MSWが無効な環境ではenableMocking()は即座に完了するため、通常通りアプリが起動します。

環境変数とスクリプトの設定

MSWの有効/無効を環境変数で制御します。Viteでは.envファイルで環境変数を設定できます。

# .env.mock(MSW有効モード用)
VITE_MSW_ENABLED=true

package.jsonにスクリプトを追加します。

{
  "scripts": {
    "dev": "vite",
    "dev:mock": "vite --mode mock"
  }
}

--mode mockを指定すると、Viteは.env.mockファイルを読み込みます。
これにより、npm run devでは通常起動、npm run dev:mockではMSW有効で起動、という切り替えが可能になります。


3. MSWインジケーターの実装(任意だが推奨)

開発中、現在MSWが有効かどうかを画面上で確認できるインジケーターコンポーネントを実装することをおすすめします。
ここでは、画面左下にMSWの状態を表示するものとします。

インジケーターの仕様

実装するインジケーターは以下の機能を持つとよいでしょう。
(プロジェクトによって最適なインジケーターを実装してください。ここではあくまで一例です。)

  • MSWが有効な場合のみ表示され、画面左下に固定表示される。
  • クリックで詳細情報を展開/折りたたみでき、本番環境では表示されない。
  • 表示内容は以下の通り。
    • MSWが有効であることを示すラベル
    • 現在のモード(全モック / 部分モック など)
    • 登録されているハンドラーの数

インジケーターコンポーネントの実装

// src/components/MswIndicator.tsx
import { useState } from 'react';

// MSWの有効状態を判定
// この関数は環境変数を参照するため、ビルド時に値が確定する
function isMswEnabled(): boolean {
  return (
    import.meta.env.DEV &&
    import.meta.env.VITE_MSW_ENABLED === 'true'
  );
}

// モード名を取得(複数モードがある場合に拡張可能)
function getMswModeName(): string {
  if (import.meta.env.VITE_MSW_FULL === 'true') {
    return 'フルモック';
  }
  if (import.meta.env.VITE_MSW_PARTIAL === 'true') {
    return '部分モック';
  }
  return 'モック有効';
}

export function MswIndicator() {
  // MSWが無効なら何も表示しない
  if (!isMswEnabled()) {
    return null;
  }

  const [isExpanded, setIsExpanded] = useState(false);
  const modeName = getMswModeName();

  return (
    <div
      style={{
        position: 'fixed',
        bottom: '16px',
        left: '16px',
        zIndex: 9999,
        fontFamily: 'system-ui, sans-serif',
        fontSize: '12px',
      }}
    >
      {/* メインボタン */}
      <button
        onClick={() => setIsExpanded(!isExpanded)}
        style={{
          display: 'flex',
          alignItems: 'center',
          gap: '6px',
          padding: '8px 12px',
          backgroundColor: '#f59e0b',
          color: '#ffffff',
          border: 'none',
          borderRadius: '6px',
          cursor: 'pointer',
          boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
          fontWeight: 'bold',
        }}
        title="クリックで詳細を表示"
      >
        {/* アイコン(丸) */}
        <span
          style={{
            width: '8px',
            height: '8px',
            backgroundColor: '#ffffff',
            borderRadius: '50%',
            animation: 'pulse 2s infinite',
          }}
        />
        MSW
      </button>

      {/* 展開時の詳細パネル */}
      {isExpanded && (
        <div
          style={{
            marginTop: '8px',
            padding: '12px',
            backgroundColor: '#fffbeb',
            border: '1px solid #f59e0b',
            borderRadius: '6px',
            boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
            minWidth: '180px',
          }}
        >
          <div style={{ marginBottom: '8px', fontWeight: 'bold', color: '#b45309' }}>
            Mock Service Worker
          </div>
          <div style={{ color: '#78350f' }}>
            <div>状態: 有効</div>
            <div>モード: {modeName}</div>
          </div>
        </div>
      )}

      {/* パルスアニメーション用のスタイル */}
      <style>{`
        @keyframes pulse {
          0%, 100% { opacity: 1; }
          50% { opacity: 0.5; }
        }
      `}</style>
    </div>
  );
}

アプリケーションへの組み込み

インジケーターをアプリケーションのルートコンポーネントに配置します。

// src/App.tsx
import { MswIndicator } from './components/MswIndicator';
// 他のimport...

function App() {
  return (
    <>
      {/* 既存のアプリケーションコンポーネント */}
      <Router>
        <Header />
        <Main />
        <Footer />
      </Router>

      {/* MSWインジケーター(MSW無効時は非表示) */}
      <MswIndicator />
    </>
  );
}

export default App;

4. ハンドラーの記述方法

この章では、様々なパターンのAPIに対応するハンドラーの書き方を解説します。

HTTPメソッド別の基本形

MSWは主要なHTTPメソッドすべてに対応しています。

import { http, HttpResponse } from 'msw';

export const handlers = [
  // GET リソースの取得
  http.get('/api/items', () => {
    return HttpResponse.json([{ id: 1, name: 'アイテム1' }]);
  }),

  // POST リソースの作成
  http.post('/api/items', () => {
    return HttpResponse.json({ id: 2, name: '新規アイテム' }, { status: 201 });
  }),

  // PUT リソースの更新(全体置換)
  http.put('/api/items/:id', () => {
    return HttpResponse.json({ id: 1, name: '更新後アイテム' });
  }),

  // PATCH リソースの部分更新
  http.patch('/api/items/:id', () => {
    return HttpResponse.json({ id: 1, name: '部分更新アイテム' });
  }),

  // DELETE リソースの削除
  http.delete('/api/items/:id', () => {
    return new HttpResponse(null, { status: 204 });
  }),
];

URLパラメータの取得

:paramNameの形式でURLパスにパラメータを定義し、ハンドラー内で取得できます。

http.get('/api/users/:userId/posts/:postId', async ({ params }) => {
  // paramsオブジェクトからパラメータを取得
  const { userId, postId } = params;

  return HttpResponse.json({
    userId: Number(userId),
    postId: Number(postId),
    title: `ユーザー${userId}の投稿${postId}`,
  });
}),

クエリパラメータの取得

/api/search?q=keywordのようなクエリパラメータは、request.urlから取得します。

http.get('/api/search', async ({ request }) => {
  // URLオブジェクトを使ってクエリパラメータを解析
  const url = new URL(request.url);
  const query = url.searchParams.get('q');
  const page = url.searchParams.get('page') ?? '1';
  const limit = url.searchParams.get('limit') ?? '10';

  return HttpResponse.json({
    query,
    page: Number(page),
    limit: Number(limit),
    results: [
      { id: 1, title: `「${query}」の検索結果1` },
      { id: 2, title: `「${query}」の検索結果2` },
    ],
  });
}),

リクエストボディの取得

POST、PUT、PATCHリクエストでは、リクエストボディを取得できます。

http.post('/api/users', async ({ request }) => {
  // JSONボディを取得
  const body = await request.json();

  console.log('受信したデータ:', body);

  // 受信したデータを使ってレスポンスを生成
  return HttpResponse.json({
    id: Date.now(),
    name: body.name,
    email: body.email,
    createdAt: new Date().toISOString(),
  }, { status: 201 });
}),

条件分岐によるシナリオ切り替え

パラメータの値に応じて異なるレスポンスを返すことで、複数のテストシナリオを実現できます。

http.get('/api/orders/:orderId', async ({ params }) => {
  const orderId = Number(params.orderId);

  // orderIdに応じて異なるシナリオを返す
  type Order = { id: number; status: string; total: number };
  const scenarios: Record<number, Order | null> = {
    1: { id: 1, status: 'pending', total: 1000 },
    2: { id: 2, status: 'processing', total: 2500 },
    3: { id: 3, status: 'shipped', total: 3200 },
    4: { id: 4, status: 'delivered', total: 1800 },
    999: null,
  };

  const data = scenarios[orderId];

  // 存在しない場合は404エラー
  if (data === null) {
    return HttpResponse.json(
      { error: '注文が見つかりません' },
      { status: 404 }
    );
  }

  // デフォルトのレスポンス
  return HttpResponse.json(data ?? scenarios[1]);
}),

レスポンス遅延のシミュレーション

ネットワーク遅延を再現するために、delay関数を使用します。

import { delay } from 'msw';

http.get('/api/slow-endpoint', async () => {
  // 3秒待機(遅いネットワークをシミュレート)
  await delay(3000);

  return HttpResponse.json({ message: '遅いレスポンス' });
}),

ローディング表示やタイムアウト処理のテストに使用できます。

エラーレスポンス

様々なエラーパターンを再現できます。

// 400 Bad Request(バリデーションエラー)
http.post('/api/users', async ({ request }) => {
  const body = await request.json();

  if (!body.email) {
    return HttpResponse.json(
      {
        error: 'バリデーションエラー',
        details: [{ field: 'email', message: 'メールアドレスは必須です' }],
      },
      { status: 400 }
    );
  }

  return HttpResponse.json({ id: 1, ...body }, { status: 201 });
}),

// 401 Unauthorized(認証エラー)
http.get('/api/protected', () => {
  return HttpResponse.json(
    { error: '認証が必要です' },
    { status: 401 }
  );
}),

// 403 Forbidden(権限エラー)
http.delete('/api/admin/users/:id', () => {
  return HttpResponse.json(
    { error: '権限がありません' },
    { status: 403 }
  );
}),

// 500 Internal Server Error(サーバーエラー)
http.get('/api/unstable', () => {
  return HttpResponse.json(
    { error: 'サーバー内部エラーが発生しました' },
    { status: 500 }
  );
}),

5. 複数モードの実装

プロジェクトによっては、「全APIをモック」「一部のAPIだけモック」など、複数のモードを切り替えるケースがあります。

モード設計

環境変数の設定

モードごとに環境変数ファイルを用意します。

# .env.mock-partial(部分モック用)
VITE_MSW_ENABLED=true
VITE_MSW_PARTIAL=true
# .env.mock(フルモック用)
VITE_MSW_ENABLED=true
VITE_MSW_FULL=true

ハンドラーセットの分離

// src/mocks/handlers/index.ts
import { commonHandlers } from './commonHandlers';
import { featureHandlers } from './featureHandlers';

// フルモック用 認証を含むすべてのハンドラー
export const handlers = [
  ...commonHandlers,
  ...featureHandlers,
];

// 部分モック用 認証は本物を使用し、機能APIのみモック
export const partialHandlers = [
  ...featureHandlers,
];

モード判定とハンドラー選択

// src/mocks/browser.ts
import { setupWorker } from 'msw/browser';
import { handlers, partialHandlers } from './handlers';

// 環境変数に応じて使用するハンドラーセットを決定
function selectHandlers() {
  if (import.meta.env.VITE_MSW_FULL === 'true') {
    return handlers;
  }
  if (import.meta.env.VITE_MSW_PARTIAL === 'true') {
    return partialHandlers;
  }
  return handlers; // デフォルト
}

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

スクリプトの追加

{
  "scripts": {
    "dev": "vite",
    "dev:mock": "vite --mode mock",
    "dev:mock-partial": "vite --mode mock-partial"
  }
}

6. トラブルシューティング

よくある問題と解決方法

MSWが起動しない場合

確認事項として、public/mockServiceWorker.jsが存在するか、環境変数VITE_MSW_ENABLED=trueが設定されているか、ブラウザのコンソールにエラーが出ていないかを確認します。

Service Workerファイルを再生成する場合は以下のコマンドを実行します。

npx msw init public/ --save

ハンドラーが効かない場合(本物のAPIが呼ばれる)

確認事項として、handlers/index.tsにハンドラーが登録されているか、URLパスが完全に一致しているか(大文字小文字、スラッシュの有無)、HTTPメソッドが正しいかを確認します。

デバッグ方法として、ハンドラー内でログを出力します。

http.get('/api/test', async ({ request }) => {
  console.log('MSWがリクエストを受信:', request.url);
  return HttpResponse.json({ test: true });
}),

古いモックデータが返される場合

対処法として、ブラウザをハードリロード(Ctrl + Shift + R)します。

それでも解決しない場合は、DevTools → Application → Service Workers → mockServiceWorker.jsを Unregister し、ページを再読み込みします。

デバッグに使用するツール

DevToolsのNetworkタブでは、リクエストの詳細(URL、メソッド、ステータス、レスポンス)を確認できます。

DevToolsのApplicationタブでは、Service Workersセクションで、MSWのService Workerが正しく登録・起動されているか確認できます。

Consoleタブでは、MSW起動時に[MSW] Mocking enabled.が表示されていれば正常に動作しています。


7. 実装上の注意点

ハンドラーの整理

機能やドメインごとにファイルを分割し、index.tsで統合する構成にします。1ファイルにすべてのハンドラーを書くと、ファイルが肥大化します。

テストデータの分離

モックで返すデータは、ハンドラーファイルとは別のファイルに切り出すと管理しやすくなります。

// src/mocks/data/users.ts
export const mockUsers = [
  { id: 1, name: '田中太郎', email: 'tanaka@example.com' },
  { id: 2, name: '鈴木花子', email: 'suzuki@example.com' },
];

// src/mocks/handlers/userHandlers.ts
import { mockUsers } from '../data/users';

http.get('/api/users', () => {
  return HttpResponse.json(mockUsers);
}),

レスポンスフォーマットの統一

プロジェクトのAPIレスポンス形式に合わせたヘルパー関数を用意すると、ハンドラーの記述が統一されます。

// ヘルパー関数
function createResponse<T>(data: T, methodName: string) {
  return {
    methodName,
    value: data,
    error: null,
  };
}

// 使用例
http.get('/api/users', () => {
  return HttpResponse.json(createResponse(mockUsers, 'GetUsers'));
}),

参考リンク

MSW公式ドキュメント

GitHub


これからもQiitaで発信していくので、気になったらぜひいいね・フォローお願いします!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?