@nwire/container
v0.15.1
Published
Nwire — DI container contract + Awilix-backed default. Generic over TCradle; ships scope hierarchy, lazy cradle proxy, lifetimes, disposers via Awilix.
Downloads
5,977
Readme
@nwire/container
Typed DI container, generic over the app's Cradle shape. Awilix-backed under the hood — you get scope hierarchy, lazy proxy cradle, lifetimes (singleton/transient/scoped), disposers, and factory-with-deps for free.
pnpm add @nwire/containerWhy
Apps grow into needing:
- Typed bindings.
container.cradle.loggershould beLogger, notunknown. - Per-request scopes. Each HTTP request gets its own scope so
user,tenant,requestIddon't leak across requests. - Lifetimes.
singletonfor connections;scopedfor per-request transactions;transientfor fresh-each-time values. - Disposers. Graceful shutdown calls
db.close(),redis.quit()automatically when a scope ends. - Source location. Studio shows "this binding was registered at
src/wires/api.ts:42".
Awilix has done this for over a decade. We wrap it with a typed Container<TCradle> interface so app code stays decoupled from awilix-specific imports, and you opt into the full Awilix surface via .raw when you need it.
Surface
interface Container<TCradle extends object = object> {
resolve<T, K extends string>(name: K): K extends keyof TCradle ? TCradle[K] : T;
register<T, K extends string>(
name: K,
factory: K extends keyof TCradle ? TCradle[K] | (() => TCradle[K]) : T | (() => T),
): void;
readonly cradle: TCradle;
createScope(): Container<TCradle>;
has(name: keyof TCradle | (string & {})): boolean;
list?(): ReadonlyArray<BindingEntry>;
readonly raw: AwilixContainer<TCradle>;
}
function createContainer<TCradle extends object = object>(): Container<TCradle>;Consumer example
import { createContainer } from "@nwire/container";
// Each plugin exports its cradle contribution as a type.
import type { AuthCradle } from "@nwire/auth"; // { "auth.user": User; … }
import type { DbCradle } from "@nwire/drizzle"; // { "db.pg": PgClient }
// App composes the cradle (plus its own bindings).
type AppCradle = AuthCradle &
DbCradle & {
config: AppConfig;
logger: Logger;
};
const root = createContainer<AppCradle>();
root.register("config", { port: 3000, dbUrl: process.env.DATABASE_URL! });
root.register("logger", () => ({ log: (s) => process.stdout.write(s + "\n") }));
root.register("db.pg", () => new PgClient(root.cradle.config.dbUrl)); // ← typed
root.cradle.logger.log(`Listening on :${root.cradle.config.port}`); // ← typed, autocomplete
// Per-request scope — child inherits, owns request-scoped values
server.on("request", async (req, res) => {
const scope = root.createScope();
scope.register("requestId", crypto.randomUUID());
scope.register("user", await loadUser(req));
// hand `scope` to your handler / framework
});Lazy by default
container.cradle is an Awilix proxy. Untouched bindings never instantiate; touched ones are cached (singleton) or fresh-each-time (transient) per the registration.
const factory = vi.fn(() => new ExpensiveDb());
root.register("db", factory);
// factory not called yet
void root.cradle.db; // ← now called once
void root.cradle.db; // ← cached, not called againFull Awilix when you need it
container.raw is the underlying AwilixContainer. Use it for disposers, asClass, scoped lifetimes, async resolution, or loadModules — anything beyond the simple register/cradle/resolve surface:
import { asFunction, asValue, Lifetime } from "awilix";
root.raw.register({
pgPool: asFunction(({ config }) => new PgPool(config.dbUrl))
.singleton()
.disposer((p) => p.end()), // graceful shutdown
pgTx: asFunction(({ pgPool }) => pgPool.transaction())
.setLifetime(Lifetime.SCOPED) // one transaction per request scope
.disposer((tx) => tx.rollback()), // auto-rollback if scope ends without commit
});Used by
Every Nwire transport (HTTP, queue, cron, MCP) creates its scope chain through this surface. Plugins register into it. Handlers read from a wire-composed ctx that pulls from the cradle.
Notes
- Awilix uses PROXY injection mode by default — destructure parameters resolve from the cradle by name.
keyof TCradlekeys can include any string, including dotted names ("auth.user","db.pg"). Namespacing is the convention to avoid plugin name clashes.
