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.
npm install @inferdi/inferdi @inferdi/honoNote:
InferDI ships on both npm and JSR. On Deno install it withdeno add jsr:@inferdi/inferdi jsr:@inferdi/hono npm:hono.
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:
Asingletoncannot depend on ascopedortransientservice — that would leak a short-lived value into a long-lived one, and InferDI rejects it at compile time. Keep request-bound servicesscoped.
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 appUse 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')
},
})
)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')))
)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. |
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()
}
})
})