InferDI
InferDI は、TypeScript 向けの依存性注入コンテナーです。デコレーター、リフレクション、実行時の依存関係を必要としません。@inferdi/hono ミドルウェアは、呼び出しごとにリクエストスコープを作成して c.var.di で公開し、ルートの処理が完了した後に破棄します。
コンテナーの型には依存関係グラフが含まれます。依存関係の不足、順序の誤り、不正なライフタイムの関係は TypeScript が報告します。
インストール
npm install @inferdi/inferdi @inferdi/honoNOTE
InferDI は JSR でも公開されています。Deno では、 deno add jsr:@inferdi/inferdi jsr:@inferdi/hono npm:hono を実行してください。
はじめに
1. コンテナーを構築する
リクエストデータは HTTP 境界で扱います。Hono の Context を依存関係グラフに含める代わりに、小さなアプリケーション用の型を定義してください。
// container.ts
import { Container } from '@inferdi/inferdi'
import { connectDatabase, UserService } from './services'
export interface RequestContext {
readonly requestId: string
readonly userId: string | undefined
}
const config = {
dsn: 'postgres://localhost/app',
} satisfies { readonly dsn: string }
export const root = new Container()
.registerValue('config', config)
.declareScopeInputs<{ request: RequestContext }>()
.registerAsyncFactory(
'db',
(config: typeof config) => connectDatabase(config.dsn),
['config']
)
.registerClass('users', UserService, ['db', 'request'], 'scoped')declareScopeInputs() は、型だけのスロットをグラフに追加し、値は登録しません。users は request に依存するため、スコープがその入力を提供するまで、InferDI はこのサービスの解決を許可しません。シングルトンはリクエストスコープのデータに依存できないため、users はスコープ単位のサービスです。
2. リクエストスコープを作成する
関数でアプリケーションが必要とする具体的なスコープを作成します。
const openRequestScope = (request: RequestContext) =>
root.createScope({ request })
type RequestScope = ReturnType<typeof openRequestScope>3. ミドルウェアを追加する
InferdiHonoScopeEnv は、具体的な型を持つ準備済みの RequestScope を Hono のコンテキスト変数で公開します。
import { Hono } from 'hono'
import { inferdiHono, type InferdiHonoScopeEnv } from '@inferdi/hono'
type AppEnv = InferdiHonoScopeEnv<RequestScope>
const app = new Hono<AppEnv>()
const createRequestScope = (userId: string | undefined) =>
openRequestScope({
requestId: crypto.randomUUID(),
userId,
})
app.use(
'*',
inferdiHono({
container: root,
createScope: (_root, c) =>
createRequestScope(c.req.header('x-user-id')),
})
)ミドルウェアがルートコンテナーの引数なしの createScope() が返す型を使う場合は、InferdiHonoEnv<typeof root> が引き続き有用です。独自のスコープファクトリーは、この例のように、より具体的な型を返せます。そのため、この例では InferdiHonoScopeEnv<RequestScope> を使います。
4. サービスを解決する
準備済みの同期キーには get()、宣言的な非同期キーには getAsync() を使います。request 入力は同期ですが、users は db に依存するため非同期です。
app.get('/users/:id', async (c) => {
const request = c.var.di.get('request') // RequestContext
const users = await c.var.di.getAsync('users') // UserService
const user = await users.profile(c.req.param('id'))
return c.json({ requestId: request.requestId, user })
})
export default appc.get('di') は c.var.di と等価で、型も同じです。
非同期の依存関係
registerAsyncFactory() は、Promise<Database> ではなく、最終的な Database 型をグラフに保存します。非同期という性質が UserService に伝播するため、どちらのサービスも getAsync() で解決します。getAsync() は準備済みの同期キーも受け付けますが、通常の同期サービスには get() を使ってください。
Promise を返す registerFactory() のコールバックは、意味が異なります。Promise 自体がサービスの値となり、グラフ上では同期のエントリーのままで、get() で解決します。
独自のコンテキストキー
key を渡すと、同じスコープを別の Hono コンテキスト変数で公開できます。独自のスコープファクトリーでも、宣言したリクエスト入力を提供する必要があります。
type CustomEnv = InferdiHonoScopeEnv<RequestScope, 'container'>
const customKeyApp = new Hono<CustomEnv>()
customKeyApp.use(
'*',
inferdiHono({
container: root,
key: 'container',
createScope: (_root, c) =>
createRequestScope(c.req.header('x-user-id')),
})
)
customKeyApp.get('/users/:id', async (c) => {
const users = await c.var.container.getAsync('users')
return c.json(await users.profile(c.req.param('id')))
})オプション
inferdiHono は、次のオプションを受け付けます。
| オプション | デフォルト | 説明 |
|---|---|---|
container | 必須 | ルートコンテナー。ミドルウェアはこれを破棄しません。 |
key | 'di' | c.var[key] と c.get(key) で使うコンテキスト変数。 |
createScope | root.createScope() | リクエストスコープを作成します。Hono から型付きの入力を渡せます。非同期でも構いません。 |
setupScope | なし | スコープの作成後、ルートハンドラーの実行前に呼ばれます。非同期でも構いません。 |
disposeScope | scope.dispose() | リクエストスコープの破棄処理を置き換えます。非同期でも構いません。 |
autoDispose | true | アプリケーション側で破棄を行う場合は false に設定するか、false を返します。 |
onDisposeError | console.error | リクエストスコープのクリーンアップ失敗を処理します。 |
ストリーミング
Hono の stream()、streamText()、streamSSE() は、コールバックが完了する前に Response を返す場合があります。レスポンスを返す前に skipInferdiDispose() を呼び出し、ストリームが終了した時点でスコープを破棄してください。
import { streamText } from 'hono/streaming'
import { skipInferdiDispose } from '@inferdi/hono'
app.get('/users/:id/export', (c) => {
skipInferdiDispose(c)
const scope = c.var.di
const id = c.req.param('id')
return streamText(c, async (stream) => {
try {
const users = await scope.getAsync('users')
const user = await users.profile(id)
await stream.write(JSON.stringify(user) ?? 'null')
} finally {
await scope.dispose()
}
})
})skipInferdiDispose() は、正常なレスポンスの場合にのみ自動クリーンアップを抑止します。レスポンスを返す前にルートが失敗した場合は、ミドルウェアがスコープを破棄します。