Skip to content

L2 — + Container (DI)

L1 + @nwire/container for typed dependency injection. Use it the moment two handlers share a dependency.

What you add

bash
pnpm add @nwire/container

The shape

ts
import { httpInterface, endpoint } from "@nwire/http"
import { Container, token } from "@nwire/container"
import { z } from "zod"

// Typed tokens — the only thing handlers depend on.
const Logger = token<{ info(msg: string): void }>("Logger")
const Db     = token<{ query(sql: string): Promise<unknown[]> }>("Db")

const container = new Container()
container.provideValue(Logger, console)
container.provideFactory(Db, () => ({ query: async () => [] }))

const api = httpInterface({ container })

api.get("/users", async ({ resolve }) => {
  const log = resolve(Logger)
  const db  = resolve(Db)
  log.info("fetching users")
  return db.query("select * from users")
})

await endpoint("hello", { port: 3000 }).serve(api).run()

What's new vs L1

  • Container holds bindings. token<T>(name) ties a type to a runtime identifier.
  • Handlers get resolve(Token) — type-safe, no any, no string keys.
  • provideValue / provideFactory / provideClass covers the common shapes.
  • Bindings can be scoped (transient / singleton / scoped per-request).

When to stay at L2

  • Your dependencies are static — created once at boot, never replaced.
  • You don't need lifecycle hooks beyond shutdown.

When to climb to L3

When a dependency needs a boot() (open a DB pool, fetch JWKS, warm a cache) or a shutdown() (close connections in reverse order), or when you want the same lifecycle hook to fire across many features, you want @nwire/app plugins.

Next

MIT licensed.