Skip to content

DI Bag ​

TypeScript dependency composition and resource ownership for modular codebases. Built for coding agents that ship one feature at a time.

Documentation · Quickstart · Modules as units of work · Comparison · Tutorial · API reference

Why DI Bag? ​

Compose ordinary TypeScript factories into reusable features. DI Bag checks declared dependencies, keeps module internals private, and manages resource creation and cleanup. Build each feature against an explicit contract, test it with replaced dependencies, and let the compiler check the composition when independently developed features come together.

Install ​

Install di-bag from npm:

sh
npm install di-bag

The API is pre-1.0 and includes breaking changes, so review the changelog and migration guides when updating.

The minimum supported TypeScript version is 6.0.3; enable strict in your tsconfig.json. The repository checks classic TypeScript 6.0.3 and native 7.0.2. For browsers and Deno, see runtime support.

Quickstart ​

A service can be a configuration object, a database client, or a function. A factory creates a service. A bag holds those factories and gives each one access to the services it needs. Services are created when needed, and resources are cleaned up when you provide a disposer and close their bag.

Import from di-bag. Here, greeter needs config. Its parameter type describes that dependency, and its return value is the service it provides:

ts
import { DiBag } from 'di-bag';

const app = DiBag.createBuilder()
  .register({
    config: () => ({ greeting: 'Hello' }),
    greeter: ({ config }: { config: { greeting: string } }) => ({
      greet(name: string) {
        return `${config.greeting}, ${name}!`;
      },
    }),
  })
  .build();

const greeter = app.resolve('greeter');
console.log(greeter.greet('Ada')); // Hello, Ada!

.register() adds factories to an immutable builder, .build() checks the declared graph and creates the bag, and resolve('greeter') creates the greeter and the config it needs. Resolving greeter again returns the same instance. Registration order does not matter.

TypeScript knows that greeter has a greet(name: string): string method. Removing the config factory makes .build() a compile-time error. Changing greeting to a number also fails the type check because the greeter needs a string.

Swap a dependency for a test ​

Use fork() to create a separate bag with a replacement dependency. Continuing the quickstart:

ts
const testApp = app.fork(['config'], {
  config: () => ({ greeting: 'Hi' }),
});

try {
  console.log(testApp.resolve('greeter').greet('Ada')); // Hi, Ada!
  console.log(app.resolve('greeter').greet('Ada')); // Hello, Ada!
} finally {
  await testApp.close();
}

The replacement must satisfy the original service contract. Each fork has independent acquisition and cleanup ownership; close it separately. Factories can still return shared objects captured outside the fork.

Work with async services ​

An async factory provides a promise. Declare that promise in any dependent factory and await it where you need the value:

ts
import { DiBag } from 'di-bag';

const app = DiBag.createBuilder()
  .register({
    greeting: async () => 'Hello',
    message: async ({ greeting }: { greeting: Promise<string> }) =>
      `${await greeting}, Ada!`,
  })
  .build();

console.log(await app.resolve('message')); // Hello, Ada!

By default, repeated resolutions share the same in-flight promise. Synchronous factories keep returning ordinary values. See async behavior for details.

Give resources a clear owner ​

Wrap a factory with withDisposal to tell the bag how to release its result:

ts
import { DiBag } from 'di-bag';

const resources = DiBag.createBuilder()
  .register({
    cache: DiBag.withDisposal(
      () => new Map<string, string>(),
      (cache) => cache.clear(),
    ),
  })
  .build();

try {
  resources.resolve('cache').set('answer', '42');
} finally {
  await resources.close();
}

The same pattern works for connections, clients, and subscriptions. Cleanup can be asynchronous. Dependents close before their dependencies, and resources that were never created need no cleanup. Ordinary factories return borrowed values; having a close() method alone does not transfer ownership to the bag.

Scopes and forks ​

OperationWhat it createsWho closes it?
bag.createScope()A tracked child with fresh scoped services; root services are sharedClose it when its work ends. The parent also closes live children.
bag.fork()An independent bag with the same registrations and fresh instancesThe caller closes it separately.
bag.fork(keys, overrides)An independent bag with selected dependencies replacedThe caller closes it separately.

For request handling, checked test replacements, and loading dynamic features, see the server guide and integration recipes.

The tutorial also covers modules with private services, typed tokens, class and function adapters, optional and lazy dependencies, collections, startup, metadata, observers, and plugin validation.

Modules as units of work ​

A DI Bag module is the unit of work that one person or one coding agent can own: a directory with a small exported contract, private services, and its own tests. The composition is checked when the modules meet, so several modules can be developed in parallel and merged with confidence.

  • A boundary an owner can hold. buildModule(keys) seals a feature and exports only the named services. Private services and their types stay inside, and two modules can use the same private names without collision.
  • Verification without the whole application. A module type-checks against the contracts it declares. fork() replaces its external dependencies with typed fixtures for deterministic tests, so a module's tests need neither the other modules nor live clients.
  • Checks at merge time. Installing every module into one builder is where independently developed work meets. A missing requirement, an incompatible replacement, or a contract that no longer matches its consumers fails at build() or verifyGraph(). The di-bag-graph tool exports the declared edges and cycles for review.

For discovery, the directory layout is the map: one directory per module, the contract first. The modularity guide describes the recommended layout and shows separately owned features, isolated tests, and contributed tools in three runnable programs.

DI Bag's dependency graph describes how services are supplied, not what runs next. LLM harnesses and agent graphs are one application of the module pattern: model clients, tools, and context sources become modules, and the harness or graph framework owns routing, retries, persistence, and execution. The agent harness and graph guide is a complete example.

How it compares ​

DI Bag's appeal is the combination of object-parameter factories, checks across the declared graph, and explicit resource ownership. Decorator-free composition, async factories, and TypeScript support are also available in other libraries.

AlternativeReasons to choose itDI Bag's different emphasis
Manual dependency injectionDirect function calls may be all a small application needs. TypeScript checks their arguments.Adds lazy caching, graph-wide composition checks, scopes, and coordinated cleanup.
AwilixFunction and class registration, inferred cradle types, lifetime options, and runtime strict checks.Checks declared factory requirements against the registrations at compile time.
InversifyJS / TSyringeToken and class-oriented containers; Inversify also offers decorator-free factory bindings and awaited async resolution.Starts with object-parameter factories and immutable builders; checks accumulated graph contracts.
Typed InjectA close alternative with compile-time dependency checks, explicit dependency tuples, child injectors, and disposal.Adds object-parameter dependencies, forward references, private module exports, and selected startup with rollback.
Effect Context / LayerTyped requirements, scoped resources, and composition within Effect's broader async and error model.Keeps ordinary T and Promise<T> service values and explicit bag lifecycles.
NestJS / Angular DITheir native containers connect directly to framework components, testing tools, and lifecycles.Provides standalone composition; applications supply the framework integration.

See the comparison guide for primary sources, differences in async and cleanup behavior, and the limits of these comparisons. There is no verified performance ranking against these libraries.

Runtime support ​

The package has zero runtime dependencies and two entry points:

ImportPurpose
di-bagThe entry to use. Configures native Promise detection itself on Node, Bun, and Deno through process.getBuiltinModule; has no node: imports, so it also bundles for browsers.
di-bag/nodeThe same API with detection configured explicitly at import, for Node and Bun.

On hosts without process.getBuiltinModule (browsers, workers), build() rejects automatic acquisition stages and names them. Register with DiBag.fromSyncFactory / DiBag.fromAsyncFactory there, or configure a trusted classifier. See portable mode.

Tradeoffs and limits ​

  • Agent context is still your responsibility. DI Bag does not choose module boundaries, manage an agent's context window, or replace behavioral tests.
  • Async dependencies are explicit. A factory returning Promise<T> exposes that promise. Consumers declare and await it themselves.
  • Cleanup waits for your work by default. Cancellation is cooperative; a factory or disposer that never settles keeps close() pending. Pass close({ timeoutMs, signal }) to stop waiting: the rejection names the disposers still running and cleanup continues in the background.
  • Type safety follows the declared graph. Casts, unchecked JavaScript, and unknown plugins need appropriate runtime checks. Dependency cycles are detected at runtime, or before running by di-bag-graph.
  • Graph types have a compiler cost. One fluent expression is bounded by the compiler's recursion budget: classic TypeScript 6.0.3 accepts about 1,000 chained calls and overflows beyond that (about 950 for a bulk map followed by individual replacements); native 7.0.2 has no such ceiling. Keep an expression to 500 calls or fewer and use bulk registration, groups, or named modules beyond that. See the compiler evidence.
  • Framework integration belongs to the application. DI Bag provides the composition and ownership primitives; the host connects request, job, or UI lifecycles. The server guide and the React guide are tested recipes for both.

Explore further ​

ResourceWhat you'll find
Complete tutorialLearn every public API through examples, from first composition to advanced ownership.
API referenceExact generated signatures, overloads, type parameters, and API inventories.
Server guideNode HTTP, Express, Fastify, Bun, and Deno: shared services, request scopes, startup, and shutdown.
React guideBrowser applications: one app runtime at bootstrap, project runtimes owned from effects, Strict Mode, cancellation, bounded teardown, and useSyncExternalStore.
Radical modularityThe recommended module layout, separately owned features, isolated tests, and contributed tools.
Agent docsRules, module layout, and check commands for coding agents, with recipes and errors. Shipped in the package.
Agent harnesses and graphsOne worked application: model and tool modules, metadata inspection, and node tests with typed fixtures.
Static dependency graphExport every builder chain, declared edge, and cycle to JSON with di-bag-graph for merge review and CI.
Runnable examplesModules, tokens, composition, collections, plugins, observers, scopes, and provider metadata.
Integration guideTested recipes for request ownership, substitutions, and dynamic features.
Comparison with alternativesWhen DI Bag or another approach may be a better fit, with primary sources.
Development and verificationFull checks, portable runtime testing, compiler scale, and performance evidence.
Documentation mapEvery guide, the generated reference, and the contributor documents.

Working on DI Bag ​

CIRuntime dependencies: 0License: MIT

After npm ci, run the main checks with Node and Bun installed:

sh
npm run platform:pin
npm run check
npm run check:native

Run an example with bun run examples/composition.ts. The development guide covers the remaining compiler, platform, and packaging checks and their tool requirements.

License ​

MIT © Dany Fedorov

Ordinary services. Checked composition. Explicit ownership.