Awilix dependency injection with scoped request containers: working versions (tested 2026-10-15)
Wire services with an Awilix container in PROXY mode using asClass, asFunction and asValue, singleton, scoped and transient lifetimes, per-request scopes and disposers.
passed5 passed · 0 failed2026-10-15 17:10 UTC- Status
- passed
- Last passed
- 2026-10-15 17:10 UTC
- Tests
- 5 / 5
- Language
- TypeScript
- Runtime
- node v22.23.3
pinned versions3
awilix@13.0.5@types/node@26.6.5tsx@4.23.15
run it
npm install --no-audit --no-fund --ignore-scripts=false npm test
test environment
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 38775053802
The code
Exactly the files that passed. Your agent can pull them with get_recipe("awilix-di-container") (install), or see them on GitHub.
package.json
{
"name": "awilix-di-container",
"private": true,
"type": "module",
"scripts": {
"test": "node --import tsx --test test/*.test.ts"
},
"dependencies": {
"awilix": "13.0.5"
},
"devDependencies": {
"@types/node": "26.6.5",
"tsx": "4.23.15"
}
}
src/container.ts
import { createContainer, asClass, asFunction, asValue, InjectionMode, Lifetime, type AwilixContainer } from "awilix";
export class Database {
closed = false;
rows = new Map<number, { id: number; name: string }>([[1, { id: 1, name: "Ada" }]]);
async close() { this.closed = true; }
}
export class UserRepository {
private db: Database;
constructor({ db }: { db: Database }) { this.db = db; }
find(id: number) { return this.db.rows.get(id) ?? null; }
}
export class UserService {
private repo: UserRepository;
private requestId: string;
constructor({ userRepository, requestId }: { userRepository: UserRepository; requestId: string }) {
this.repo = userRepository;
this.requestId = requestId;
}
get(id: number) { return { requestId: this.requestId, user: this.repo.find(id) }; }
}
export type Cradle = {
config: { appName: string };
db: Database;
userRepository: UserRepository;
userService: UserService;
requestId: string;
greeter: (name: string) => string;
};
export function buildContainer(): AwilixContainer<Cradle> {
const container = createContainer<Cradle>({ injectionMode: InjectionMode.PROXY, strict: true });
container.register({
config: asValue({ appName: "provenpin" }),
db: asClass(Database, { lifetime: Lifetime.SINGLETON }).disposer((db) => db.close()),
userRepository: asClass(UserRepository).singleton(),
userService: asClass(UserService).scoped(),
requestId: asValue("none"),
greeter: asFunction(({ config }: Cradle) => (name: string) => `${config.appName} says hi to ${name}`).transient(),
});
return container;
}
/** One scope per incoming request, with its own requestId value. */
export function handleRequest(container: AwilixContainer<Cradle>, requestId: string, userId: number) {
const scope = container.createScope();
scope.register({ requestId: asValue(requestId) });
return scope.resolve("userService").get(userId);
}
test/container.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { asValue } from "awilix";
import { buildContainer, handleRequest, Database } from "../src/container.ts";
test("resolves the dependency graph", () => {
const c = buildContainer();
assert.deepEqual(handleRequest(c, "req-1", 1), { requestId: "req-1", user: { id: 1, name: "Ada" } });
assert.equal(c.resolve("greeter")("Bob"), "provenpin says hi to Bob");
});
test("lifetimes: singletons shared, scoped per scope, transient always new", () => {
const c = buildContainer();
const a = c.createScope();
const b = c.createScope();
assert.equal(a.resolve("db"), b.resolve("db"));
assert.equal(a.resolve("userService"), a.resolve("userService"));
assert.notEqual(a.resolve("userService"), b.resolve("userService"));
assert.notEqual(c.resolve("greeter"), c.resolve("greeter"));
});
test("overrides for tests replace a registration", () => {
const c = buildContainer();
const fake = new Database();
fake.rows.set(1, { id: 1, name: "Fake" });
c.register({ db: asValue(fake) });
assert.equal(handleRequest(c, "t", 1).user?.name, "Fake");
});
test("dispose runs disposers for resolved singletons", async () => {
const c = buildContainer();
const db = c.resolve("db");
await c.dispose();
assert.equal(db.closed, true);
});
test("strict mode reports missing registrations", () => {
const c = buildContainer();
assert.throws(() => c.resolve("missing" as never), /Could not resolve 'missing'/);
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.