InferDI
InferDI is a zero-dependency, decorator-free, strongly typed dependency injection container for TypeScript. The @inferdi/hono middleware wires it into Hono's request pipeline: it creates one DI scope per request, exposes it on the context as c.var.di, and disposes it after the response completes — no decorators, reflection, or route scanning.
The graph is the type: a misordered dependency, a missing key, or a request-scoped value leaking into a singleton are all compile errors, not runtime surprises.
🛠️ Installation
npm install @inferdi/inferdi @inferdi/honoNOTE
InferDI ships on both npm and JSR. On Deno install it with deno add jsr:@inferdi/inferdi jsr:@inferdi/hono npm:hono.
🚀 Getting Started
1. Build a container
Register your services on a root Container. Dependencies are passed as a tuple of keys and type-checked positionally against the constructor — a wrong order or type is a compile error. Each registration declares a lifetime: singleton (default, one instance per container), scoped (one per request), or transient (new on every resolve).
// container.ts
import { Container } from '@inferdi/inferdi'
export function buildRootContainer() {
return (
new Container()
.registerClass('logger', Logger, [])
// `request` is scoped: a fresh instance per request scope.
.registerClass('request', RequestContext, [], 'scoped')
// `users` is scoped too — it depends on the scoped `request`.
.registerClass(
'users',
UserService,
['logger', 'request'],
'scoped'
)
)
}NOTE
A singleton cannot depend on a scoped or transient service — that would leak a short-lived value into a long-lived one, and InferDI rejects it at compile time. Keep request-bound services scoped.
2. Add the middleware
inferdiHono creates a request scope before your handlers run and disposes it afterwards. InferdiHonoEnv<typeof root> types c.var.di as your concrete scope, so .get(key) stays fully typed.
import { Hono } from 'hono'
import { inferdiHono, type InferdiHonoEnv } from '@inferdi/hono'
import { buildRootContainer } from './container'
const root = buildRootContainer()
const app = new Hono<InferdiHonoEnv<typeof root>>()
app.use('*', inferdiHono({ container: root }))
export default app3. Hydrate the request scope
Use setupScope to fill request-scoped services with per-request data (request id, authenticated user, …) before any handler sees the scope. It runs once per request and may be async.
app.use(
'*',
inferdiHono({
container: root,
setupScope: (scope, c) => {
const request = scope.get('request')
request.requestId = crypto.randomUUID()
request.userId = c.req.header('x-user-id')
},
})
)4. Resolve services in handlers
Resolve any registered service from the request scope with c.var.di.get(key). The returned value is fully typed, and scoped services share one instance for the whole request.
app.get('/users/:id', async (c) => {
const user = await c.var.di.get('users').profile(c.req.param('id'))
return c.json(user)
})c.get('di') is equivalent to c.var.di. To use a different context key, pass key and reflect it in the env type:
type AppEnv = InferdiHonoEnv<typeof root, 'container'>
const app = new Hono<AppEnv>()
app.use('*', inferdiHono({ container: root, key: 'container' }))
app.get('/users/:id', (c) =>
c.json(c.var.container.get('users').profile(c.req.param('id')))
)⚙️ Options
inferdiHono accepts the following options:
| Option | Default | Description |
|---|---|---|
container | — | Required. The root container. The middleware never disposes the root. |
key | 'di' | Context variable key used for c.var[key] / c.get(key). |
createScope | root.createScope() | Overrides how the request scope is created. May be async. |
setupScope | — | Hydrates the scope before handlers run. May be async. |
disposeScope | scope.dispose() | Overrides request-scope disposal. May be async. |
autoDispose | true | Set to false (or return false) when application code owns disposal. |
onDisposeError | console.error | Sink for post-response disposal failures. |
🌊 Streaming
A streaming response returns before the stream callback finishes, so disable auto-disposal with skipInferdiDispose(c) and dispose the scope yourself when the stream ends.
import { stream } from 'hono/streaming'
import { skipInferdiDispose } from '@inferdi/hono'
app.get('/events', (c) => {
skipInferdiDispose(c)
const scope = c.var.di
const events = scope.get('events')
return stream(c, async (s) => {
try {
for await (const event of events.subscribe()) {
await s.write(`data: ${JSON.stringify(event)}\n\n`)
}
} finally {
await scope.dispose()
}
})
})