Angular v18にmswを導入する手順

https://mswjs.io/

Mock Service Worker（以下MSW）はHTTP通信をモックする開発用ツールです。フロントエンドでエラー表示やページネーションなどを作り込む時に、欲しいレスポンスがかんたんにシミュレートできて重宝します。

この記事では、AngularでMSWをセットアップする手順と、モックのカスタマイズをいくつか紹介します。

MSWには、サーバーサイドのNode.jsと、ブラウザのService Workerの2通りのセットアップ手順があり、この記事ではブラウザを対象としています。

- `@angular/cli` v18
- `msw` v2
- `Node.js` v22

# インストール

既存プロジェクトにパッケージをインストールします。

```bash
npm i msw -D 
```

# initコマンド実行

CLIでコマンドを実行して `mockServiceWorker.js` を追加します。
このファイルはブラウザ上のService Workerとして動作します。

```bash
npx msw init ./src --save

./src/
├── app
│   ├── app.component.html
│   ├── app.config.ts
│   └── ...
├── index.html
├── main.ts
├── mockServiceWorker.js # <=== これ
├── mocks
│   └── handlers.ts
```

ブラウザから参照できるよう `angular.json` に登録します。

```ts:angular.json
"architect": {
	"build": {
    "configurations": {
      "development": {
        "assets": [
+          "src/mockServiceWorker.js"
        ]
      }
    },
```

# ハンドラの追加

HTTP通信をモックする「ハンドラ」用のファイルを追加します。

```bash
mkdir src/mocks
touch src/mocks/handlers.ts 
```

ハンドラの中身は後述します。ここでは空で宣言しておきます。

```ts:src/mocks/handlers.ts
export const handlers = [];
```

# MSWを起動

ハンドラを呼び出すスクリプトを追加します。

```bash
touch src/mocks/browser.ts
```

```ts:src/mocks/browser.ts

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

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

`main.ts` を以下のように書き換えます。

```ts:src/main.ts

import { isDevMode } from "@angular/core";

async function prepareApp() {
	// ローカル開発環境に限定
  if (isDevMode()) {
    const { worker } = await import("./mocks/browser");
    return worker.start();
  }

  return Promise.resolve();
}

// Angularアプリケーションを起動する直前にService Workerを立ち上げる
prepareApp().then(() => {
  bootstrapApplication(AppComponent, appConfig).catch((err) =>
    console.error(err)
  );
});

```

ここまでの手順で、Angularプロジェクトのディレクトリ構造は以下のようになります。
●印が付いているファイルはMSWのセットアップによって改変されたファイルです。

```bash
├── README.md
├── ● angular.json
├── node_modules/
├── ● package-lock.json
├── ● package.json
├── public/
├── src/
│   ├── app/
│   ├── index.html
│   ├── ● main.ts
│   ├── ● mockServiceWorker.js
│   ├── mocks/
│   │   ├── ● browser.ts
│   │   └── ● handlers.ts
│   └── styles.scss
├── tsconfig.app.json
├── tsconfig.json
└── tsconfig.spec.json
```

MSWのGitHubでAngularアプリケーションのサンプルコードが提供されています。
公式ドキュメントで見当たらないものはこちらを参考にすると良さそうです。

[https://github.com/mswjs/examples/with-angular](https://github.com/mswjs/examples/tree/bd4fb5a70d2975e0877c657879a4afbb6b76b0b0/examples/with-angular)

# HTTP通信してみる

任意のコンポーネントにHTTP通信の処理を書いて、MSWのモックを試してみましょう。

```ts:src/app/app.config.ts

import { provideHttpClient } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [
+    provideHttpClient(),
  ]
};
```

```ts:src/app/app.component.ts

import { HttpClient } from '@angular/common/http';
import { Component, inject, OnInit, signal } from '@angular/core';

@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    @for (user of users(); track $index) {
      <p>{{ user.name }}</p>
    }
  `,
})
export class AppComponent implements OnInit {
  private readonly api = inject(HttpClient);

  users = signal<User[]>([]);

  ngOnInit() {
    this.api.get<User[]>("/api/users").subscribe(users => {
	    this.users.set(users);
	});
  }
}

type User = {
  id: number;
  name: string;
};
```

さきほど空にしておいたハンドラを記述します。

```ts:src/mocks/handlers.ts

import { http, HttpResponse } from "msw";

export const handlers = [
  http.get("/api/users", () => {
    return HttpResponse.json([
      { id: 1, name: "Bob" },
      { id: 2, name: "Alice" },
    ]);
  }),
];
```

ハンドラ宣言は以下のようなシンタックスです。
エンドポイントひとつ分を表します。

```ts
http.`{メソッド}`(`{URL}`, () => {
	return HttpResponse.json(`{レスポンス}`);
});
```

エンドポイントを追加するには、配列にハンドラを追加します。

```ts
export const handlers = [
  http.get("/api/users", () => { ... }),
  http.get("/api/users/:id", () => { ... }),
  http.post("/api/users", () => { ... }),
  http.put("/api/users/:id", () => { ... }),
  http.delete("/api/users/:id", () => { ... }),
];
```

Angularアプリケーションを起動してブラウザを確認してみましょう。

```bash
npm start
```

`GET /api/users` に対して、モックのレスポンスが返ります。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102284/c13db9c2-7cb0-9a81-9a11-80d1e6c3dbd7.png)

# モックのカスタマイズ

MSWの魅力は、モックのカスタマイズの容易さです。
イレギュラーを含むさまざまなケースで画面表示を作り込む時に活躍します。

- 巨大なリストが返るケース
- レスポンスが遅延するケース
- クエリ文字列でページネーション指定するケース
- パスパラメータに応じたデータを返すケース
- HTTP 500が発生するケース
- ポーリングするケース

通信用にバックエンドのアプリケーションのコードを改変したり、データベースにダミーデータを登録する必要はありません。TypeScriptで自由自在にモックが作れる便利さは偉大です。

またPOSTやDELETEをリクエストしてもデータが永続化されないので、ブラウザをリロードするだけで「最初からやり直し」ができます。

# 巨大なリストが返るケース

サイズの大きいリストを生成してレスポンスで返却します。
フィクスチャを宣言しても良いですし、TypeScriptで動的生成することもできます。

```ts
export const handlers = [
  http.get("/api/users", () => {
    const users = Array.from({ length: 10000 }).map((_, index) => {
      return {
        id: index + 1,
        name: `User ${index + 1}`,
      };
    });

    return HttpResponse.json(users);
  }),
];

// GET /api/users
// [
//   { id: 1, name: "User 1" },
//   { id: 2, name: "User 2" },
//   { id: 3, name: "User 3" },
//   ...(10000件)...
//  ]
```

# レスポンスが遅延するケース

ユーティリティ関数 `delay` を使います。

[https://mswjs.io/docs/api/delay/](https://mswjs.io/docs/api/delay/)

```ts
import { delay } from "msw";

export const handlers = [
  http.get("/api/users", async () => {
    await delay(10000);

    return HttpResponse.json([{ id:1, name: "Bob" }]);
  }),
];

// GET /api/users
// ...(10秒)...
// [{ id:1, name: "Bob" }]
```

# クエリ文字列でページネーション指定するケース

コールバック関数の引数で `request` オブジェクトを受け取り、クエリ文字列を取り出します。

[https://mswjs.io/docs/recipes/query-parameters/](https://mswjs.io/docs/recipes/query-parameters/)

```ts
export const handlers = [
  http.get("/api/users", ({ request }) => {
    const users = [{}, {}, {}, ...];

    const page = Number(new URL(request.url).searchParams.get("page"));
    const perPage = 10;
    const first = perPage * (page - 1);
    const last = first + perPage;

    return HttpResponse.json(users.slice(first, last));
  }),
];

// GET /api/users?page=1
// [{}, {}, {}, ...]
```

# パスパラメータに応じたデータを返すケース

コールバック関数の引数で `params` オブジェクトを受け取り、パラメータを取り出します。

[https://mswjs.io/docs/network-behavior/rest/#reading-path-parameters](https://mswjs.io/docs/network-behavior/rest/#reading-path-parameters)

```ts
export const handlers = [
  http.get("/api/users/:id", ({ params }) => {
    const { id } = params;

    switch (Number(id)) {
      case 1: return HttpResponse.json({ id: 1, name: "Bob" });
      case 2: return HttpResponse.json({ id: 2, name: "Alice" });
      default: ...
    }
  }),
];

// GET /api/users/1
// { id: 1, name: "Bob" }

// GET /api/users/2
// { id: 2, name: "Alice" }
```

# HTTP 500が発生するケース

`HttpResponse.json` のショートハンドを使わずに `new HttpResponse()` すると、ステータスコードやレスポンスボディを指定できます。

[https://mswjs.io/docs/api/http-response](https://mswjs.io/docs/api/http-response)

```ts
export const handlers = [
  http.get("/api/users", () => {
    return new HttpResponse('Critical Error!', {
      status: 500,
    });
  }),
];

// GET /api/users
// HTTP 500 Critical Error!
```

# ポーリングするケース

ジェネレーター関数を使ってN回分のリクエストをシミュレートします。
ポーリング中にHTTP 500が発生するケースも試せますね。

[https://mswjs.io/docs/recipes/polling](https://mswjs.io/docs/recipes/polling)

```ts
export const handlers = [
  http.get("/api/users", function* () {
    const list = [
      { id: 1, name: "Bob" },
      { id: 2, name: "Alice" },
      { id: 3, name: "John" },
    ];

    let count = 0;

    while (count < 3) {
      yield HttpResponse.json(list.slice(0, count + 1));
      count++;
    }

    return HttpResponse.json(list);
  }),
];

// GET /api/users
// [{ id: 1, name: "Bob" }]

// GET /api/users
// [{ id: 1, name: "Bob" }, { id: 2, name: "Alice" }]

// GET /api/users
// [{ id: 1, name: "Bob" }, { id: 2, name: "Alice" }, { id: 3, name: "John" }]

```

# fetch or XHR

[https://mswjs.io/docs/limitations/](https://mswjs.io/docs/limitations/)

公式ドキュメントにはfetchをトリガしないHTTP通信について注意が記載されていますが、Angularの `withFetch` を指定しても（fetch API）、指定しなくても（XMLHttpRequest）、特に変わりなくMSWが動作しました。

```ts:src/app/app.config.ts

import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withFetch } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [ 
    provideHttpClient(withFetch()), // <===
  ]
};
```

Chrome 129.0.6668.101

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102284/332118d4-7b1f-c81f-c972-48692d1c6842.png)


Firefox 131.0.3

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102284/eb6bb0fd-1ccf-e7f2-6f48-1bda95adb1df.png)


MSWの正常な稼働を確認するには、ブラウザのコンソールタブで `[MSW] HH:mm:ss {URL}` のログが出力された事を確認します。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102284/3701ef98-cba1-b732-45a7-a397d490b7f9.png)


ログはHTTP通信のたびに吐き出されるため、初回確認の後はオフにしておくとコンソールが静かになります。

```ts:src/main.ts

return worker.start({ quiet: true });
```

---

以上です。
かんたん便利にHTTP通信をモックできるMSW、気になった方はぜひ使ってみてください。