バックエンドの API がまだできてないプロジェクトで Next のフロントを作成することになり開発中の API との通信に噂の MSW を使ってみることにしました。
Next.js 特有の問題でちょいハマりしたので導入完了したところまでのメモ。
MSW (Mock Service Worker)
MSW(Mock Service Worker)はブラウザリクエストを Service Worker がインターセプトし、任意のレスポンスを返すことが出来るライブラリです。
cf. MSW で加速するフロントエンド開発
プロダクションのコードのまま API への通信だけを Service Worker がインターセプトしてモックのレスポンスを返すことができるので、バックエンドと切り離してフロントだけを開発することができます!すごい〜
MSW の導入
$ npm i -D msw
開発時だけ使うので -D
でインストール
MSW の Service Worker を生成
npx msw init <公開ディレクトリ> --save
で MSW が使用する Service Worker が生成される
Next アプリの公開ディレクトリは public
なので下記のようにすれば OK
$ npx msw init public --save
/public
に mockServiceWorker.js
が生成され、package.json に msw: {"workerDirectory": "public"}
というコードが追加される。
これで MSW が使えるようになるので後はモック API を作成していくだけ
note.
Service Worker を生成してないと次のようなエラーが表示されます。
Error: [MSW] Failed to register a Service Worker for scope ('http://localhost:3000/') with script ('http://localhost:3000/mockServiceWorker.js'): Service Worker script does not exist at the given path.
Did you forget to run "npx msw init <PUBLIC_DIR>"?
Learn more about creating the Service Worker script: https://mswjs.io/docs/cli/init
モック API の作成
// src/mocks.js import { setupWorker, rest } from 'msw' const worker = setupWorker( rest.post('/login', (req, res, ctx) => { const isAuthenticated = sessionStorage.getItem('username') if (!isAuthenticated) { return res( ctx.status(403), ctx.json({ errorMessage: 'Not authenticated', }), ) } return res( ctx.json({ firstName: 'John', }), ) }), ) // Register the Service Worker and enable the mocking worker.start()
cf. Introduction - Mock Service Worker Docs
setupWorker
の中にモックの API を定義して、worker.start()
すればモック API に合致するリクエストは MSW がインターセプトしてモック API に書いている値を返すことができる。
モックの API を全部同じファイルに書くのは大変なので、下記のような構成で作成しました。
/src |- /mock |- /api | |- auth.ts … モックの API の処理 |- handler.ts … API handler 設定 (routing) |- worker.ts … worker.start() するファイル
モックの API の処理
/src/mock/api/auth.ts
import { MockedRequest, ResponseResolver, restContext } from 'msw'; import jwt, { SignOptions } from 'jsonwebtoken'; export const mockLogin: ResponseResolver<MockedRequest, typeof restContext> = async (req, res, ctx) => { const token = await jwt.sign({ uid: 1 }, JWT_SERCRET_KEY, {expiresIn: '1h'}); return res( ctx.status(200), ctx.cookie(COOKIE_NAME, token, { httpOnly: true, path: '/' }), ctx.json({ name: '星宮いちご', } )); }; export const mockLogout: ResponseResolver<MockedRequest, typeof restContext> = (req, res, ctx) => { return res( ctx.status(200), ctx.cookie(COOKIE_NAME, null, { expires: new Date(0), }), ctx.json('logout') ); };
API handler 設定 (routing)
/src/mock/handler.ts
import { rest } from 'msw'; import { mockLogin, mockLogout } from './api/auth'; import { API } from '@/config.ts'; export const handlers = [ rest.post(`${API}/login`, mockLogin), rest.post(`${API}/logout`, mockLogout), ];
Mock Service Worker のメインファイル
/src/mock/worker.ts
import { setupWorker } from 'msw'; import { handlers } from './handler'; export const worker = setupWorker(...handlers);
処理を分けることで見通しが良くなった気がします!
Next アプリの開発モードの時だけ MSW を有効にする
api 呼び出しはアプリ全体で使うのでアプリのルートである /src/pages/_app.tsx
で process.env.NODE_ENV === 'development'
の時だけ worker を有効にすれば OK
/src/pages/_app.tsx
import { VFC } from 'react'; import { AppProps } from 'next/app'; if (process.env.NODE_ENV === 'development') { // dynamic import でファイルを読み込んで MSW を有効にする const MockServer = () => import('@/mock/browser').then((mo) => { mo.worker.start(); }); MockServer(); } const App: VFC<AppProps> = ({ Component, pageProps }) => { return (<Component {...pageProps} />); }; export default App;
これで MSW が有効に…となるつもりだったのですが、コンソールに次のようなエラーが表示されていました。
(node:12365) UnhandledPromiseRejectionWarning: Error: [MSW] Failed to execute setupWorker
in a non-browser environment. Consider using setupServer
for Node.js environment instead.
サーバーサイドでは Service Worker が使えないのが原因
Next は SSR できるようにサーバーサイドの動作もしているので、サーバーサイド時でもこの Service Worker を使おうとしているけどできないよ!ということのようです。
サーバーサイドでは setupWorker
ではなく setupServer
を使うようにする
MSW はサーバーサイドでも使える様になっており、node 環境では setupServer.listen()
とすれば良いようです。
Next アプリは 1 つのコードでサーバーサイドもクライアントサイドでも実行されるので、node 環境かどうかの判定して切り替えるよう変更します。
/src/mock/browser.ts
と /src/mock/server.ts
を作成して /src/mock/worker.ts
内で切り替えられるように変更します。
/src |- /mock |- /api | |- auth.ts … モックの API の処理 |- handler.ts … API handler 設定 (routing) |- browser.ts … クライアントサイド用 |- server.ts … サーバーサイド用 |- worker.ts … worker.start() するファイル
/src/mock/browser.ts
import { setupWorker } from 'msw'; import { handlers } from './handler'; export const worker = setupWorker(...handlers);
/src/mock/server.ts
import { setupServer } from 'msw/node'; import { handlers } from './handler'; export const server = setupServer(...handlers);
/src/mock/worker.ts
export {}; if (typeof window === 'undefined') { const { server } = require('./server'); server.listen(); } else { const { worker } = require('./browser'); worker.start(); }
worker.ts
内で実行させるようにしたので、読み込ませる箇所も修正する
/src/pages/_app.tsx
import { VFC } from 'react'; import { AppProps } from 'next/app'; if (process.env.NODE_ENV === 'development') { const MockServer = () => import('@/mock/worker'); MockServer(); }
これで Next アプリで開発モードの時は API へのリクエストすれば MSW からモックの JSON を取得できるようになりました!
₍ ᐢ. ̫ .ᐢ ₎ ヤッタゼ!!
Auth で cookie を返す方法はコレが良いのか解ってない。
[参考]
Jungle Moc 履き心地いいよね。