Skip to content

Express アダプター

@inferdi/express は Express 5 のミドルウェアです。1 つのリクエストスコープを作成し、それを req.di として公開し、Node レスポンスが finish または close した後に破棄します。

インストール

bash
pnpm add @inferdi/inferdi @inferdi/express express
pnpm add -D @types/express
ts
import express from 'express'
import { Container } from '@inferdi/inferdi'
import { inferdiExpress } from '@inferdi/express'

リクエストスコープ

ts
type RequestContext = {
  requestId: string
  userId?: string
  ip?: string
}

class Users {
  constructor(readonly request: RequestContext) {}

  profile(id: string) {
    return { id, userId: this.request.userId }
  }
}

const root = new Container()
  .declareScopeInputs<{ request: RequestContext }>()
  .registerClass('users', Users, ['request'], 'scoped')
const openRequestScope = (request: RequestContext) =>
  root.createScope({ request })
type RequestScope = ReturnType<typeof openRequestScope>

declare global {
  namespace Express {
    interface Request {
      di: RequestScope
    }
  }
}

const app = express()

app.use(inferdiExpress({
  container: root,
  createScope: (_root, req) => openRequestScope({
    requestId: crypto.randomUUID(),
    userId: req.get('x-user-id') || undefined,
    ip: req.ip
  })
}))

app.get('/users/:id', async (req, res, next) => {
  try {
    res.json(await req.di.get('users').profile(req.params.id))
  } catch (error) {
    next(error)
  }
})

このアダプターは、Express.Requestanyunknown、またはベースコンテナでグローバルに拡張することはありません。アプリケーションが自身の具体的なリクエスト型を所有します。

オプション

オプションデフォルト説明
container必須ルートコンテナ。このミドルウェアによって破棄されることはありません。
createScoperoot.createScope()カスタムのリクエストスコープ作成。
setupScopeなしスコープ作成後に追加の初期化を行います。
disposeScopescope.dispose()カスタムの破棄。
autoDisposetruefalse または false を返す述語は所有権を移譲します。
onDisposeErrorconsole.errorクリーンアップ失敗のシンク。

ストリーミングとバックグラウンド作業

通常の Express ストリームレスポンスにはスキップは不要です。アダプターが finish または close を待つためです。

作業が意図的に HTTP レスポンスより長く生き残る場合は、skipInferdiDispose(req) を使用してください。

ts
import { skipInferdiDispose } from '@inferdi/express'

app.get('/background', (req, res) => {
  skipInferdiDispose(req)
  const scope = req.di

  queue.add(async () => {
    try {
      await scope.get('jobs').run()
    } finally {
      await scope.dispose()
    }
  })

  res.status(202).json({ status: 'queued' })
})

失敗したリクエストに関する注意点

他のアダプターとは異なり、Express は処理済みのルートエラーに対して、スキップされたスコープを確実に強制破棄することができません。Express のミドルウェアはコールバックスタイルです。next() が戻った後、アダプターは後でエラーハンドラーによって処理されたダウンストリームの例外を観測できません。ルートが skipInferdiDispose(req) を呼び出してから失敗した場合、スコープはアプリケーション所有のままになります。自身のエラーパスからそれを破棄するか、スキップとスローし得るルートを組み合わせるのを避けてください。

skipInferdiDispose(req) を一度呼ぶと、そのリクエストのすべての InferDI ミドルウェアインスタンスに適用されます。すべてのスコープをアプリケーション側で保持して破棄してください。req.di は最後のミドルウェアが設定したスコープを公開します。