Skip to content

@nwire/container — alone

Typed dependency injection. No decorators, no reflect-metadata, no class metadata fishing. Just typed tokens and three provideX methods.

What it does

  • token<T>(name) creates a typed identifier — the only thing your code depends on.
  • container.provideValue / provideFactory / provideClass registers a binding.
  • container.resolve(token) returns a T, type-checked at compile time.
  • Scopes (transient / singleton / scoped per child container) supported.
  • Child containers (container.child()) for per-request scopes.

Install

bash
pnpm add @nwire/container

Minimal example

ts
import { Container, token } from "@nwire/container"

interface Logger { info(msg: string): void }
interface Db     { query(sql: string): Promise<unknown[]> }

const LoggerT = token<Logger>("Logger")
const DbT     = token<Db>("Db")

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

const log = container.resolve(LoggerT)
const db  = container.resolve(DbT)

log.info("ready")
await db.query("select * from users")

Class providers

ts
class UserService {
  constructor(private db: Db, private log: Logger) {}
  list() { this.log.info("listing"); return this.db.query("select * from users") }
}
const UserServiceT = token<UserService>("UserService")

container.provideClass(UserServiceT, UserService, [DbT, LoggerT])  // typed deps array

Per-request scope

ts
const RequestId = token<string>("RequestId")

http.use((req, res, next) => {
  const scope = container.child()
  scope.provideValue(RequestId, req.headers["x-request-id"] ?? uuid())
  req.scope = scope
  next()
})

// Inside a handler:
const id = req.scope.resolve(RequestId)

What this is not

  • It's not a service locator — you don't import { container } from "./container" everywhere. Pass the container or its tokens.
  • It's not an IoC framework with auto-wiring — you declare bindings explicitly, by hand. The reward is no reflect-metadata, no decorators, no class-metadata edge cases.
  • It's not opinionated about lifetimes — you choose scoped vs singleton per binding.

When to use this and not the L3 @nwire/app plugin form

The L3 definePlugin(...) form wraps provide + boot + shutdown + on together so a single closure packages binding + lifecycle. If your dependencies are static (created once, never replaced, no boot work, no shutdown work), @nwire/container alone is enough. The moment you need lifecycle, climb to L3.

MIT licensed.