# RSDI - Simple & Strong-Type Dependency Injection Container

[![npm version](https://img.shields.io/npm/v/rsdi.svg)](https://www.npmjs.com/package/rsdi)
[![CI](https://github.com/radzserg/rsdi/actions/workflows/ci.yml/badge.svg)](https://github.com/radzserg/rsdi/actions/workflows/ci.yml)
[![license](https://img.shields.io/npm/l/rsdi.svg)](./LICENSE)

Manage your dependencies with ease and safety. RSDI is a minimal, powerful DI container with full TypeScript support — no decorators or metadata required.

> **Using an AI coding agent?** Point it at
> **[docs/ai-agent-guide.md](./docs/ai-agent-guide.md)** — a single-page integration guide covering
> the API, the mistakes that don't compile, how to structure a large container, and how to decode
> RSDI's error messages. Every example in it is compile- and runtime-verified.

- [Motivation](#motivation)
- [Features](#features)
- [Installation](#installation)
- [Best Use Cases](#best-use-cases)
- [Architecture](#architecture)
- [How to use](#how-to-use)
- [Strict types](#strict-types)
- [Advanced Usage](#advanced-usage)
  - [Extend](#extend)
  - [Compose](#compose)
  - [Merge](#merge)
  - [Clone](#clone)
  - [Naming your container type](#naming-your-container-type)
  - [Other methods](#other-methods)
- [Further reading](#further-reading)

## Motivation

Most DI libraries rely on reflect-metadata and decorators to auto-wire dependencies. But this tightly couples
your business logic to a framework — and adds complexity:

```typescript
@injectable()
class Foo {
  constructor(@inject('Database') private database?: Database) {}
}
// Notice how in order to allow the use of the empty constructor new Foo(),
// we need to make the parameters optional, e.g. database?: Database.
```

Why should your core logic even know it's injectable?

RSDI avoids this by using explicit factory functions — keeping your code clean, framework-agnostic, and easy to test.

[Read more](https://radzserg.medium.com/https-medium-com-radzserg-dependency-injection-in-react-part-2-995e93b3327c)

## Features

- No decorators
- Strong TypeScript support
- Simple API
- No runtime dependencies
- Easy to mock and test
- Scales to large graphs — [compose](#compose) independent modules instead of one long chain

## Installation

```bash
npm install rsdi
# or
pnpm add rsdi
# or
yarn add rsdi
```

```typescript
import { DIContainer } from 'rsdi';
```

## Best Use Cases

Use `RSDI` when your app grows in complexity:

- You break big modules into smaller ones
- You have deep dependency trees (A → B → C)
- You want to pass dependencies across layers:
  - Controllers
  - Domain managers
  - Repositories
  - Infrastructure services

## Architecture

`RSDI` works best when you organize your app as a dependency tree.

A typical backend app might have:

- Controllers (REST or GraphQL)
- Domain managers (use-cases, handlers)
- Repositories (DB access)
- Infrastructure (DB pools, loggers)

![architecture](https://github.com/radzserg/rsdi/raw/main/docs/RSDI_architecture.jpg 'RSDI Architecture')

Set up your DI container at the app entry point — from there, all other parts can pull in what they need.

## How to use

### Basic Example

```typescript
const container = new DIContainer()
  .add('a', () => 'name1')
  .add('bar', () => new Bar())
  .add('foo', ({ a, bar }) => new Foo(a, bar));

const { foo } = container; // alternatively  container.get("foo");
```

### Real-World Example

```typescript
// sample web application components

export function UserController(userRegistrator: UserRegistrator, userRepository: UserRepository) {
  return {
    async create(req: Request, res: Response) {
      const user = await userRegistrator.register(req.body);
      res.send(user);
    },
    async list(req: Request, res: Response) {
      const users = await userRepository.findAll(req.body);
      res.send(users);
    },
  };
}

export class UserRegistrator {
  public constructor(public readonly userRepository: UserRepository) {}

  public async register(userData: SignupData) {
    // validate and send sign up email
    return this.userRepository.saveNewUser(userData);
  }
}

export function MyDbProviderUserRepository(db: DbConnection): UserRepository {
  return {
    async saveNewUser(userAccountData: SignupData): Promise<void> {
      await db('insert').insert(userAccountData);
    },
  };
}

export function buildDbConnection(): DbConnection {
  return connectToDb({/* db credentials */});
}
```

Now let's configure the dependency injection container. Dependencies are only created when they're actually needed.
Your `configureDI` function will declare and connect everything in one place.

```typescript
import { DIContainer } from 'rsdi';

export type AppDIContainer = ReturnType<typeof configureDI>;

export default function configureDI() {
  return new DIContainer()
    .add('dbConnection', () => buildDbConnection())
    .add('userRepository', ({ dbConnection }) => MyDbProviderUserRepository(dbConnection))
    .add('userRegistrator', ({ userRepository }) => new UserRegistrator(userRepository))
    .add('userController', ({ userRepository, userRegistrator }) =>
      UserController(userRegistrator, userRepository),
    );
}
```

When a resolver runs for the first time, its result is cached and reused for future calls.

By default, you should always use `.add()` to register dependencies — it throws if the name already exists, which
prevents accidental overwrites and keeps your setup predictable. If you need to replace an existing dependency —
usually in tests — use `.update()` instead:

```typescript
const container = configureDI();

// override a real dependency with a stub in tests
container.update('userRepository', () => new InMemoryUserRepository());
```

Let's map our web application routes to configured controllers

```typescript
// configure Express router
export default function configureRouter(app: core.Express, diContainer: AppDIContainer) {
  const { userController } = diContainer;
  app.route('/users').get(userController.list).post(userController.create);
}
```

Add `configureDI()` in your app's entry point:

```typescript
// express.ts
const app = express();

const diContainer = configureDI();
configureRouter(app, diContainer);

app.listen(8000);
```

🔗 Full example: [Express + RSDI](https://radzserg.medium.com/dependency-injection-in-express-application-dd85295694ab)

## Strict types

`RSDI` uses TypeScript's type system to validate dependency trees at compile time, not runtime.

![strict type](https://github.com/radzserg/rsdi/raw/main/docs/RSDI_types.png 'RSDI types')

This gives you autocomplete and safety without decorators or metadata hacks.

## Advanced Usage

As your application grows, it's a good idea to split your DI container setup into smaller, focused modules. This keeps
your codebase easier to navigate and maintain.

A common pattern is to keep a main `diContainer.ts` file that configures the base container and delegate domain-specific
dependencies to separate files like `dataAccess.ts`, `validators.ts`, or `controllers.ts`.

This modular structure improves testability, readability, and clarity on how dependencies are wired across your app.

---

### Extend

You can extend a container with more dependencies using `.extend()`. This is ideal for building up your container in logical steps.

```ts
// diContainer.ts

export const configureDI = async () => {
  return (await buildDatabaseDependencies())
    .extend(addDataAccessDependencies)
    .extend(addValidators);
};
```

```ts
// addDataAccessDependencies.ts

export type DIWithPool = Awaited<ReturnType<typeof buildDatabaseDependencies>>;

export const addDataAccessDependencies = async () => {
  const pool = await createDatabasePool();
  const longRunningPool = await createLongRunningDatabasePool();

  return new DIContainer()
    .add('databasePool', () => pool)
    .add('longRunningDatabasePool', () => longRunningPool);
};
```

```ts
// addValidators.ts

export type DIWithValidators = ReturnType<typeof addValidators>;

export const addValidators = (container: DIWithPool) => {
  return container
    .add('myValidatorA', ({ a, b, c }) => new MyValidatorA(a, b, c))
    .add('myValidatorB', ({ a, b, c }) => new MyValidatorB(a, b, c));
};
```

> **On long chains, annotate the return type.** Deriving each module's input from the previous module's
> output (`(c: ReturnType<typeof previousModule>) => …`) is tempting, but it keeps every module's type nested
> inside the one before it, so instantiation depth accumulates down the whole chain and your build becomes
> coupled to RSDI's internals. Past a handful of modules this shows up as `TS2589` or a container that
> collapses to `never`. Give each module an explicit named return type — `(c: DIWithPool): DIWithValidators
=> …` — so every boundary flattens the accumulated type. A [`SealedContainer`](#naming-your-container-type)
> boundary every few modules gets most of the same benefit.

---

### Compose

`DIContainer.compose()` combines independently built containers into one new container. It is the recommended way to
wire a large dependency graph, and it keeps the inputs untouched.

```ts
// repositories.ts
export const repositories = new DIContainer().add('userRepository', () => new UserRepository());

// services.ts — declare what this module expects the composed container to provide
export const services = new DIContainer<{ userRepository: UserRepository }>().add(
  'userService',
  ({ userRepository }) => new UserService(userRepository),
);

// container.ts
const container = DIContainer.compose(repositories, services);

container.userService; // UserService — fully typed
```

Resolution stays lazy and happens against the composed container, so a factory may depend on names provided by any of
the composed modules. Only the _types_ of a module are limited to what that module declares — annotate the module (as
`services` does above) or use `.extend()` when you need another module's types to be visible while writing it.

If several containers define the same name, the last one wins at runtime — including over a value an
earlier container had already resolved. Be aware the _types_ intersect rather than overwrite, so the
same name registered with two different types resolves to `never` instead of the later type. That
surfaces the collision rather than hiding it; if a replacement is intentional, use `.update()`.

#### Why compose instead of one long chain

Each `.add()` widens the container type, so a single chain of N dependencies costs **O(N²)** to type-check. Splitting
the graph into modules keeps each chain short, and the compiler only pays the quadratic within a module. Measured with
TypeScript 7 on this repo's benchmark (1600 dependencies, no factory arguments):

| Layout                         | Type instantiations | Check time |
| ------------------------------ | ------------------: | ---------: |
| one chain of 1600              |           7,815,223 |     90.2 s |
| 80 modules of 20, then compose |             361,552 |      0.9 s |

That is ~22× fewer instantiations and ~100× faster. If your editor feels sluggish in the file that wires your
container, this is usually why.

**Splitting alone is not the win — isolation is.** A module is cheap when it is checked against only the
dependencies it declares it consumes. In a production app with ~340 dependencies, splitting a flat chain into
`.extend()` modules that thread the whole accumulated container measured no better than the flat chain
(3.41M → 3.48M instantiations); giving each leaf an explicit consumed-type seed and combining with `compose`
made those leaves **~13× cheaper to check** (924.8 ms → 68.7 ms). Declare the seed as an explicit interface —
`Pick<FullContainer, 'a' | 'b'>` forces TypeScript to normalise the entire accumulated map and couples the
module to the whole graph. See the
[AI agent integration guide](./docs/ai-agent-guide.md) for the full pattern.

---

### Merge

You can merge containers to combine their resolvers and resolved values. Unlike `compose`, `merge` mutates and returns
the container it is called on.

- Dependencies from all containers are preserved.
- If several define the same key, the last one takes precedence at runtime (and any value the
  replaced resolver had already produced is evicted). Types intersect, so a key defined twice with
  different types becomes `never`.
- Already resolved values are reused — not re-created.

```ts
const containerA = new DIContainer().add('a', () => '1').add('bar', () => new Bar());

const containerB = new DIContainer().add('b', () => 'b').add('buzz', () => new Buzz('buzz'));

const finalContainer = containerA.merge(containerB);

console.log(finalContainer.a); // "1"
console.log(finalContainer.b); // "b"
console.log(finalContainer.bar instanceof Bar); // true
console.log(finalContainer.buzz.name); // "buzz"
```

`merge` accepts several containers at once, which avoids a long chain of merges:

```ts
const finalContainer = base.merge(repositories, services, controllers);
```

---

### Clone

Use `.clone()` to create a new container that shares resolvers and already resolved values with the original.

This is useful for creating isolated execution contexts while preserving the base setup.

```ts
const containerA = new DIContainer()
  .add('a', () => '1')
  .add('bar', () => new Bar())
  .add('buzz', () => new Buzz('buzz'));

const containerB = containerA.clone();

console.log(containerB.a); // "1"
console.log(containerB.bar instanceof Bar); // true
console.log(containerB.buzz.name); // "buzz"
```

---

### Naming your container type

Consumer files usually want to refer to the built container by name:

```ts
export type AppDIContainer = ReturnType<typeof configureDI>;
```

That works, but TypeScript still expands it in diagnostics, so a container with a few hundred dependencies produces
errors and hovers like `IDIContainer<{ a: string; } & { b: number; } & … }>`. Wrapping it in `SealedContainer` creates a
fresh alias that TypeScript prints by name instead:

```ts
import { type SealedContainer } from 'rsdi';

export type AppDIContainer = SealedContainer<ReturnType<typeof configureDI>>;

// error messages and hovers now say `AppDIContainer`
export function configureRouter(app: core.Express, container: AppDIContainer) {
  const { userController } = container; // still fully typed
}
```

The dependency types are unchanged — `container.userController` resolves exactly as before, and the container stays
chainable.

This is purely for readability. Referring to a container from another file is already cheap: in a 400-dependency graph,
50 consumer files add only ~2.6K type instantiations each, and naming the type costs about 3% _more_, not less. Reach
for it when your error messages get unreadable, not to speed up compilation — for that, see [Compose](#compose).

---

### Other methods

- **`.get(name)`** — resolve a dependency by name. Equivalent to property access (`container.foo`). Throws
  `DependencyIsMissingError` if the name isn't registered.
- **`.has(name)`** — returns `true` if a resolver is registered under `name` (whether or not it has been resolved yet).
- **`.hasResolvedDependency(name)`** — returns `true` only if the dependency has already been resolved and cached.
- **`.update(name, resolver)`** — replace an existing dependency's resolver (see [How to use](#how-to-use)). Unlike
  `.add()`, it expects the name to already exist.

```typescript
const container = new DIContainer().add('bar', () => new Bar());

container.has('bar'); // true
container.hasResolvedDependency('bar'); // false — not resolved yet

container.get('bar');
container.hasResolvedDependency('bar'); // true — now cached
```

---

## Further reading

- **[AI agent integration guide](./docs/ai-agent-guide.md)** — one page an AI coding agent can read
  before wiring RSDI into a project: API reference, the mistakes that fail to compile, how to
  structure a large container, and how to decode each error.
- [Async factory resolvers](./docs/async_factory_resolver.md) — why factories are synchronous, and
  how to handle resources that need `await`.
- [DI container vs context](./docs/context_vs_container.md) — why the container stays at the
  composition root instead of being passed through your app.
- [Strict types](./docs/strict_types.md) — what the compiler catches for you.
- [Type-performance plan](./docs/type-performance-plan.md) — measurements behind the O(N²) chain
  cost and the composition guidance, for contributors.
- [Reading `pnpm bench:types`](./docs/type-benchmarks.md) — how to interpret the type-cost gate,
  for contributors.
