🎉

Next.jsにおけるモックサーバ

2022/05/21に公開

Next.js で使えそうなモックサーバを試してみた備忘録

実施環境
項目 詳細
PC MacBook Pro(14 インチ、2021)Apple M1 Pro
OS MacOS Monterey 12.3
Node.js v16.14.2
Next.js v12.1.4

概要

Next.js 開発でバックエンドが出来ていないケースで、モックサーバどうしようとなったので軽く比較

モックサーバ 概要
API Routes Next.js に最初から入っている機能。/pages/api 配下を使う。
JSON server 一般的なモックサーバ
MSW ブラウザリクエストをインターセプトして任意のレスポンスを返す

結論

MSW 使おうかなと思います。

API Routes

Next.js に最初から入っている機能のため、何か追加等する必要なし。
/pages/api 配下だけはページではなく、API エンドポイントとして扱われる機能。
サーバサイドのみでバンドルされるので、クライアントサイドのサイズには関係ないらしいです。

/pages/api/hello.ts
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'

type Data = {
  name: string
}

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>
) {
  res.status(200).json({ name: 'John Doe' })
}

HTTP メソッドで出し分ける場合は、req.methodを使用する

export default function handler(req, res) {
  if (req.method === 'POST') {
    // Process a POST request
  } else {
    // Handle any other HTTP method
  }
}

Next(SSR)でのサンプル

axios の baseUrl をローカルホストにする

/libs/axios/index.ts
import axios from 'axios';

export const customAxios = axios.create({
  timeout: 1 * 60 * 1000,
  baseURL: "http://localhost:3000/api",
});

slug は本番 API と揃えて設定

/pages/test.tsx
import { customAxios } from '@/libs/axios';

...

const Test: NextPage<Props> = ({data}) => {
  return (
    <div>
      <p>{data.name}</p>
    </div>
  );
}

export const getServerSideProps: GetServerSideProps = async () => {
  const res = await customAxios('/hello');
  const { data } = await res;

  // Pass data to the page via props
  return { props: { data } }
}

API Routes まとめ

  • 公式の想定用途
    • 普通に API エンドポイントとして
    • API Routes でプロキシして外部サービスの URL をマスキングする
    • 環境変数を使用してセキュアに外部サービスにアクセスする

設定は簡単。というより Next.js であれば何もする必要がない。が、上記の通りモックサーバとしての使用を想定してなさそうなので、微妙かもしれない…。
環境ごとの出し分けは必要そう。

JSON server

まずは json-server を入れる

// 公式ではグローバルにインストールしていた
npm install -g json-server

// 私は一応プロジェクトにインストール
yarn add --dev json-server

レスポンスデータを用意

db.json
{
  "posts": [
    { "id": 1, "title": "json-server", "author": "typicode" }
  ],
  "comments": [
    { "id": 1, "body": "some comment", "postId": 1 }
  ],
  "profile": { "name": "typicode" }
}

スクリプト追加。上記作成したファイルを指定する。

package.json
  "scripts": {
    ...
    "json-server": "json-server --watch db.json --port 4000"
  },

yarn json-server で起動して使う

Next(SSR)でのサンプル

API routes とほぼ一緒。baseUrl のポートが変わるくらい。
axios の baseUrl をローカルホストにする

/libs/axios/index.ts
import axios from 'axios';

export const customAxios = axios.create({
  timeout: 1 * 60 * 1000,
  baseURL: "http://localhost:4000/api",
});

JSON server まとめ

API routes との違い

  • 別プロセスでローカルサーバを立てる必要あり(そのため環境には依存しない)
  • 別のサーバなので、特に環境等で出し分ける必要がない
  • サクッと試したいときにはよさそう。

MSW

Mock Service Worker の略。
実際にローカルホストでモック用のサーバーを起動するのではなく、サービスワーカーレベルでリクエストをインターセプトしてリクエストを返却するらしいです。

インストール

npm install msw --save-dev
# or
yarn add msw --dev

どれをインターセプトするか指定するファイルの作成

src/mocks/handlers.js ファイルを作成して、インターセプトする URL、レスポンスを指定する。

src/mocks/handlers.js
import { rest } from 'msw'

export const handlers = [
  rest.post('/login', (req, res, ctx) => {
    // Persist user's authentication in the session
    sessionStorage.setItem('is-authenticated', 'true')

    return res(
      // Respond with a 200 status code
      ctx.status(200),
    )
  }),

  rest.get('/user', (req, res, ctx) => {
    // Check if the user is authenticated in this session
    const isAuthenticated = sessionStorage.getItem('is-authenticated')

    if (!isAuthenticated) {
      // If not authenticated, respond with a 403 error
      return res(
        ctx.status(403),
        ctx.json({
          errorMessage: 'Not authorized',
        }),
      )
    }

    // If authenticated, return a mocked user details
    return res(
      ctx.status(200),
      ctx.json({
        username: 'admin',
      }),
    )
  }),
]

クライアントサイドでインターセプトするための設定

下記を実行することで、mockServiceWorker.jsが作成される。
public/の部分は公開ディレクトリを指定する。

npx msw init public/ --save

src/mocks/browser.js を作成

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

// This configures a Service Worker with the given request handlers.
export const worker = setupWorker(...handlers)

Node 側でインターセプトするための設定

src/mocks/server.js ファイルを作成

src/mocks/server.js
// src/mocks/server.js
import { setupServer } from 'msw/node'
import { handlers } from './handlers'

// This configures a request mocking server with the given request handlers.
export const server = setupServer(...handlers)

アプリでインポート

アプリ側でインポートする。
その際サーバ側とクライアント側で出し分ける必要がある。(process.browser は deprecate になったらしく、typeof window での出し分けがいいとのことでした。)
開発環境のみで使用するものなので、環境変数でも出し分ける。

src/pages/_app.js
// src/pages/_app.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'

if (process.env.NODE_ENV === 'development') {
  if (typeof window === 'undefined') {
    const { server } = require('../mocks/server')
    server.listen()
  } else {
    const { worker } = require('../mocks/browser')
    worker.start()
  }
}

ReactDOM.render(<App />, document.getElementById('root'))

実際にリクエストする

/src/pages/_app.js
import { useState } from 'react'

export default function Home({ book }) {
  const [reviews, setReviews] = useState(null)

  const handleGetReviews = () => {
    // Client-side request are mocked by `mocks/browser.js`.
    fetch('/reviews')
      .then((res) => res.json())
      .then(setReviews)
  }

  return (
    <div>
      <img src={book.imageUrl} alt={book.title} width="250" />
      <h1>{book.title}</h1>
      <p>{book.description}</p>
      <button onClick={handleGetReviews}>Load reviews</button>
      {reviews && (
        <ul>
          {reviews.map((review) => (
            <li key={review.id}>
              <p>{review.text}</p>
              <p>{review.author}</p>
            </li>
          ))}
        </ul>
      )}
    </div>
  )
}

export async function getServerSideProps() {
  // Server-side requests are mocked by `mocks/server.js`.
  const res = await fetch('https://my.backend/book')
  const book = await res.json()

  return {
    props: {
      book,
    },
  }
}

Example

いくつか環境ごとのサンプルがあるので、かなり参考になると思います。上記も Next.js のサンプルから持ってきたものです。

MSW まとめ

設定は他のものより少し苦労したしあまり理解出来ていないですが、一度設定してしまえばモックサーバを意識せずに開発できるのがよさそう。handlers.js で特定のエンドポイントのみを指定してインターセプトできるので、baseUrl を書き換えたりといったことをする必要がない。モックに向けるために本番コードに何か手を加えなくていいのはとてもいいと思いました。

結論:再

設定はちょっと大変ですが、やっぱり本来のエンドポイントを叩きにいけるというのは良いなと思いました。
MSW 使ってみようかと思います。

項目 API Routes JSON server MSW
設定の簡単さ
環境によらない - (Next の機能)
本番コードに与える影響

Discussion