ProvenPin Install

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

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.