🐺

HonoのVariablesでDependency Injectionを実装する

に公開

はじめに

Webフレームワークである 🔥Hono を使ったAPI開発において、Dependency Injection(DI)をVariablesを使って実装する方法について素振りした記事です。

HonoのVariablesとは

Honoのコンストラクタには型安全性を確保するため、変数をジェネリックとして渡すことができます。
そしてミドルウェアで依存の注入を行うことができます。

type Variables = {
  message: string
}

const app = new Hono<{ Variables: Variables }>()

app.use(async (c, next) => {
  c.set('message', 'Hono is cool!!')
  await next()
})

app.get('/', (c) => {
  const message = c.get('message')
  return c.text(`The message is "${message}"`)
})

※同じリクエスト内でのみ保持されます。異なるリクエスト間で共有したり永続化したりすることはできません。

https://hono.dev/docs/api/context#set-get

DIパターンの実装

基本的なサービス層の定義

ここではDIコンテナで管理したいサービスを定義します。

// services/UserService.ts
export interface IUserService {
  getUser(id: string): Promise<{ id: string; name: string }>
}

export class UserService implements IUserService {
  async getUser(id: string) {
    // データベースアクセスのロジックがあるとして
    return { id, name: `User${id}` }
  }
}

DIコンテナの作成

サービスを管理するコンテナを作成します。

// di/container.ts
import { IUserService, UserService } from '../services/UserService'

export interface Container {
  userService: IUserService
}

export function createContainer(): Container {
  return {
    userService: new UserService(),
  }
}

Honoアプリケーションでの活用

HonoのVariablesにDIコンテナを注入します。

// app.ts
import { Hono } from 'hono'
import { Container, createContainer } from './di/container'

type Variables = {
  container: Container
}

const app = new Hono<{ Variables: Variables }>()

// DIコンテナを注入するミドルウェア
app.use('*', async (c, next) => {
  c.set('container', createContainer())
  await next()
})

// ユーザー取得API
app.get('/users/:id', async (c) => {
  const id = c.req.param('id')
  const { userService } = c.get('container')
  
  const user = await userService.getUser(id)
  return c.json(user)
})

export default app

シングルトンパターンの実装

Contextオブジェクトはリクエストごとにインスタンス化され、レスポンスが返されるまで保持されるという特徴があります。

https://hono.dev/docs/api/context#context

パフォーマンス最適化が必要な場合は、シングルトンパターンも検討するのも良さそうです。

// di/container.ts
let containerInstance: Container | null = null

export function getContainer(): Container {
  if (!containerInstance) {
    containerInstance = createContainer()
  }
  return containerInstance
}
// app.ts
app.use('*', async (c, next) => {
  c.set('container', getContainer())
  await next()
})

終わりに

HonoでDIパターン検討していたのでちょうど良さそうなVariablesを使った方法をまとめました。
他にもHonoのDIライブラリもあるようなので、要件に応じて使い分けるのが良さそうです。

Discussion