技術メモ

日々学んだことのメモを残します。

Vue3 + TypeScript + MSWの試しメモ(2)

1. はじめに

前回はVueのプロジェクトを立ち上げて、そこにMSWを導入するところまで行いました。

今回はサンプルアプリを元に、MSWでモックAPIを実装していきたいと思います。

2. サンプルアプリの内容

MSWを用いるサンプルアプリとしてTodoアプリを用います。アプリの仕様は以下の通りです。

  • Todoの一覧を取得し表示(GET)
  • Todoの追加(POST)
  • API接続中に「Loading」と表示

サンプルのTodoアプリ

3. モックAPIの作成

最初にモックAPIを実装していきます。

3.1. モックデータの用意

Todoのモックデータを用意します。モックデータのファイルもmockフォルダの配下にまとめておきます。 src/mock/data/todoMockData.json

[
  {
    "id": 1,
    "title": "Todo-1"
  },
  {
    "id": 2,
    "title": "Todo-2"
  },
  {
    "id": 3,
    "title": "Todo-3"
  }
]

idとタイトルだけのシンプルなjsonです。

3.2. handlerの実装

次にMSWのリクエストハンドラーを実装していきます。Todoの一覧を返すGETと、Todoに追加するPOSTのハンドラーを追加します。

3.2.1. ハンドラーの定義

今回はREST APIで作成します。

src/mock/handler.ts

import { rest } from "msw";

export const handlers = [
  // GETハンドラー
  rest.get('/todos', null),

  // POSTハンドラー
  rest.post('/todos', null),
]

3.2.2. レスポンスリゾルバの実装

APIのリクエストを処理するために、HTTPメソッドとパスの定義が完了しました。次にget、postのnullになっている第二引数にモック化したレスポンスを返す関数を定義します。

src/mock/handler.ts

import { rest } from "msw";
import todosMockData from "./data/todosMockData.json" // モックデータ

export const handlers = [
  // GETハンドラー
  rest.get('/todos', (req, res, ctx) => {
    // 
    return res(
      ctx.delay(2000), // レスポンスを2秒遅らせる
      ctx.status(200), // 200のステータスコードを設定
      ctx.json(todosMockData) // モックデータをレスポンスボディに設定
    )
  }),
  // POSTハンドラー
  rest.post('/todos', async(req, res, ctx) => {
    const { todo } = await req.json(); // 入力したTodoの文字列取得
    return res(
      ctx.status(201), 201のステータスコードを設定
      ctx.json({ 
        id: Math.random(), 
        title: todo,
      })
    )
  })
]

レスポンスを返す関数(レスポンスリゾルバ)ではreq, res, ctxを引数にとります。
この中でctxはモックのレスポンスを作成するためのResponse transformerが含まれています。これを使用することで、レスポンスのステータスコードやヘッダ、ボディなどを設定することができたり、レスポンスを遅延させるdelayなども設定することができます。今回はAPI接続時のローディングも確認したいので、delayを加えました。

3.2.3. TypeScriptの適用

最後に各ハンドラーにTypeScriptを適用します。 src/mock/handler.ts

import { rest } from "msw";
import todosMockData from "./data/todosMockData.json"
// Todoの型をApp.vueで定義しているのでimport
import type { Todo as postResponseBody } from "../App.vue"

interface Todo {
  todo: string
}
export const handlers = [
  // GETハンドラー
  rest.get<never, never, postResponseBody[]>('/todos', (req, res, ctx) => {
    return res(
      ctx.delay(2000),
      ctx.status(200),
      ctx.json(todosMockData)
    )
  }),
  // POSTハンドラー
  rest.post<Todo, never, postResponseBody>('/todos', async(req, res, ctx) => {
    const { todo } = await req.json<Todo>();
    return res(
      ctx.status(201),
      ctx.json({ 
        id: Math.random(),
        title: todo,
      })
    )
  })
]

MSWのハンドラーにはRequest Body、Request Params, Response Bodyの型を定義します。
以下の記事を参考に記述を行いましたが、最近のバージョンではRequest ParamsとResponse Bodyが逆になっているので注意が必要です。(Discussionsにその旨の記載があります。)

以上でモックAPIの作成は完了です。

4. Vueコンポーネントの作成

Vueコンポーネントは以下の通りです。普通のWeb APIに接続するときと同様の記述になっていますので、説明は省略します。
Todoの型定義はsrc/mock/handler.tsでも使用するのでexportしています。

src/App.vue

<template>
  <!-- API通信中はLoadingを表示 -->
  <template v-if="apiLoading">
    <div>Loading...</div>
  </template>
  <template v-else>
    <ul v-if="todos.length > 0">
      <template v-for="todo in todos">
        <li>{{ todo.title }}</li>
      </template>
    </ul>
  </template>
  <form @submit.prevent="handleSubmit">
    <input type="text" v-model="inputValue" />
    <button type="submit">追加</button>
  </form>
</template>

<script setup lang="ts">
import { ref, onMounted } from "vue";
export type Todo = {
  id: number;
  title: string;
};

const apiLoading = ref<Boolean>(false);
const todos = ref<Todo[]>([]);
const inputValue = ref<string>("");

onMounted(() => {
  apiLoading.value = true;
  fetch("/todos")
    .then((res) => {
      if (!res.ok) {
        throw new Error(res.statusText);
      } else {
        return res.json().then((data: Todo[]) => {
          todos.value = data;
        });
      }
    })
    .catch((error) => {
      console.log(error);
    })
    .finally(() => {
      apiLoading.value = false;
    });
});

const handleSubmit = () => {
  apiLoading.value = true;
  fetch("/todos", {
    method: "POST",
    body: JSON.stringify({
      todo: inputValue.value,
    }),
  })
    .then((res) => {
      if (!res.ok) {
        throw new Error(res.statusText);
      } else {
        return res.json().then((data: Todo) => {
          todos.value = [...todos.value, data];
        });
      }
    })
    .catch((error) => {
      console.log(error);
    })
    .finally(() => {
      apiLoading.value = false;
    });
};
</script>

5. 終わりに

MSWを使ってモックAPIを作成してみましたが、スムーズに実装することができました。
実際の開発で使用する際には、以下の内容の画面表示を確認できるようにモックAPIを作成するルールにすれば、画面実装時の考慮漏れが防げると思いました。

  • 正常時のレスポンス
  • 異常時のレスポンス
  • レスポンスとして受け取った配列が空の場合
  • ネットワークエラーの場合(参考:networkError)

"レスポンスとして受け取った配列が空の場合"の画面表示は、そもそものデザインの段階で考慮されてないといったこともあるので、モックAPIで確認できるようにしておけば、早い段階で気づくことができます。
またネットワークエラーは再現がしづらいので、簡単に確認ができるのは便利だと思いました。

6. 参考