そうだ Next.js 13でDependency Injection、しよう。
はじめに
最近、Next.js 13のプロジェクトにDependency Injection(DI)を導入してみました。現時点ではNext.js 13 + DIについて、まだあまり情報が見当たらない気がしたので記事にしてみました。
誰向けなのか
- DIは知っている
- Next.js 13でDIしたい
InversifyJSの導入
いきなりですがInversifyJSを使います
InversifyJSはTypeScriptでもっとも利用者が多いといわれているDIライブラリーです。
実は今回いくつかのDIライブラリーを試したのですが、あくまで現時点ではありますが、Next.js 13への導入についてはInversifyJSがいちばんスムーズに感じました。そのため、この記事ではこちらをAPIで動作確認する部分まで紹介できればと思います。(TSyringeも動くことは動きました)
プロジェクトを用意する
既存プロジェクトの場合は該当ディレクトリに移動します。
$ cd ./my-app
新規プロジェクトの場合はnpx create-next-app
し、プロジェクトのディレクトリに移動します。
$ npx create-next-app
√ What is your project named? ... my-app
√ Would you like to use TypeScript with this project? ... No / Yes
√ Would you like to use ESLint with this project? ... No / Yes
√ Would you like to use Tailwind CSS with this project? ... No / Yes
√ Would you like to use `src/` directory with this project? ... No / Yes
√ Use App Router (recommended)? ... No / Yes
√ Would you like to customize the default import alias? ... No / Yes
$ cd ./my-app
必要なパッケージをインストールする
npm install inversify reflect-metadata
なお、この時点での各バージョンは以下になります。
{
"dependencies": {
// ...
"inversify": "^6.0.1",
"next": "13.4.3",
// ...
"reflect-metadata": "^0.1.13",
// ...
"typescript": "5.0.4"
},
}
tsconfig.jsonを更新する
tsconfig.json
にてcompilerOptions.experimentalDecorators
とcompilerOptions.emitDecoratorMetadata
を有効にします。
./tsconfig.json
{
"compilerOptions": {
// ...
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
// ...
},
}
インターフェイスと型を宣言する
なにか適当にインターフェイスと型を用意します。ここでは公式に習ってinterfaces.ts
とtypes.ts
を作ります。
./src/interfaces.ts
export interface Warrior {
fight(): string
sneak(): string
}
export interface Weapon {
hit(): string
}
export interface ThrowableWeapon {
throw(): string
}
./src/types.ts
const TYPES = {
Warrior: Symbol.for('Warrior'),
Weapon: Symbol.for('Weapon'),
ThrowableWeapon: Symbol.for('ThrowableWeapon'),
}
export { TYPES }
クラスを用意してデコレーターで依存関係を宣言する
なにか適当にクラスを用意し、依存関係を宣言します。ここも公式に習って以下のentities.ts
を作ります。なお、プロジェクト構成の関係でimport
周りが少しだけ変わっています。
./src/entities.ts
import { injectable, inject } from 'inversify'
import type { Weapon, ThrowableWeapon, Warrior } from './interfaces'
import { TYPES } from './types'
@injectable()
class Katana implements Weapon {
public hit() {
return 'cut!'
}
}
@injectable()
class Shuriken implements ThrowableWeapon {
public throw() {
return 'hit!'
}
}
@injectable()
class Ninja implements Warrior {
private _katana: Weapon
private _shuriken: ThrowableWeapon
public constructor(
@inject(TYPES.Weapon) katana: Weapon,
@inject(TYPES.ThrowableWeapon) shuriken: ThrowableWeapon
) {
this._katana = katana
this._shuriken = shuriken
}
public fight() {
return this._katana.hit()
}
public sneak() {
return this._shuriken.throw()
}
}
export { Ninja, Katana, Shuriken }
DIコンテナーを作成して構成する
DIコンテナーを構成するファイルinversify.config.ts
を作ります。ここでは、これまで作ったインターフェイス・型・クラスを用いて、myContainer
を生成するように構成します。
./src/inversify.config.ts
import { Container } from 'inversify'
import { TYPES } from './types'
import { Warrior, Weapon, ThrowableWeapon } from './interfaces'
import { Ninja, Katana, Shuriken } from './entities'
const myContainer = new Container()
myContainer.bind<Warrior>(TYPES.Warrior).to(Ninja)
myContainer.bind<Weapon>(TYPES.Weapon).to(Katana)
myContainer.bind<ThrowableWeapon>(TYPES.ThrowableWeapon).to(Shuriken)
export { myContainer }
サーバー起動時にDIコンテナーを初期化する
現行Next.js 13.4では、新しいInstrumentation機能を有効にすることでサーバー起動時に任意の処理を行うことができるようです。
まずはnext.config.js
でexperimental.instrumentationHook
をtrue
にします。
./next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
instrumentationHook: true
}
}
module.exports = nextConfig
次にinstrumentation.ts
を作成し、reflect-metadata
とinversify.config
のimport
するように構成します。
./src/instrumentation.ts
import 'reflect-metadata'
export async function register() {
if (process.env['NEXT_RUNTIME'] === 'nodejs') {
await import('./inversify.config')
}
}
これでinversify.config
はサーバー起動時に読み込まれ、myContainer
が生成されるようになります。今回のDIコンテナーの構成そのままだと有効性はあまりない気がしますが、例えばサーバー起動時に予め生成したインスタンスを登録しておきたいケースなどはありがちかなと想像しており、そういった場合は有効性が出てくるかと思います。
なお、inversify.config
は副作用があるためNEXT_RUNTIME
がnodejs
の場合にawait
しています。詳しくは以下をご参照ください。
APIで動作確認する
なにか適当にAPIを用意します。ここでは/api/hello
で呼び出せるAPIができるよう、以下のhello.ts
ファイルを作ります。
./src/pages/api/hello.ts
import { NextApiRequest, NextApiResponse } from 'next'
import { myContainer } from '../../inversify.config'
import { Warrior } from '../../interfaces'
import { TYPES } from '../../types'
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const ninja = myContainer.get<Warrior>(TYPES.Warrior)
res.status(200).json({
ninja: {
fight: ninja.fight(),
sneak: ninja.sneak(),
},
})
}
次にdevサーバーを起動します。
$ npm run dev
最後にhttp://localhost:3000/api/hello
にGETリクエストを投げてレスポンスを確認します。
$ curl http://localhost:3000/api/hello
{"ninja":{"fight":"cut!","sneak":"hit!"}}
ご覧のとおり、Katanaと はShuriken正常に解決され、Ninjaに挿入されました。
サンプルプロジェクト
今回のサンプルプロジェクトは以下に置いてあります。
所感
DIはプロジェクトの規模や状況によっては必須ではないと思いますが、直近のプロジェクトではDIの導入により、プロジェクト全体の見通しが向上したと感じます。Next.js 13はまだ新しいバージョンであり、情報が不足していると感じますが、これからの成長が楽しみなフレームワークの1つです。また新しいトピックや面白そうな情報があれば共有していければと思います。この記事が誰かの役に立つことがあれば幸いです。
Discussion