---
url: https://masyaka.github.io/injecute/guide/middleware-events.md
description: >-
  Wrap resolution with middlewares (tracing, fallbacks, deprecations, guards,
  test doubles) and observe containers with events.
---

# Middlewares and events

A **middleware** wraps every resolution: it can time it, change it, or answer for keys that are not
registered. An **event listener** only observes. Reach for events first.

## Rules

* `use(middleware)` adds a middleware; `unuse(middleware)` removes it. The last added runs first.
* A middleware receives `(key, next, context)`. Call `next()` to continue, `next(otherKey)` to resolve
  another key. `context` has `container` (read-only), `path` (keys being resolved) and `depth`.
* The chain runs **once per `get()`**, including for nested dependencies (with a higher `depth`).
* Forks **inherit** their ancestors' middlewares live: one added to the parent later applies to existing
  forks. `fork({ middlewares: false })` opts out.
* Dependencies of a service registered in a parent are resolved **in the parent**, through the parent's
  middlewares (the parent owns that instance). In an [isolated fork](./containers.md#isolated-forks) they
  are resolved in the fork.

## Tracing and timing

```ts
const tracing: Middleware = (key, next, { depth }) => {
  const start = performance.now();
  const value = next();
  const ms = (performance.now() - start).toFixed(1);
  lines.push(`${'  '.repeat(depth)}${String(key)} ${ms}ms`);
  return value;
};

const app = new DIContainer()
  .use(tracing)
  .addInstance('config', { url: 'postgres://localhost' })
  .addSingleton('db', (config) => ({ config }), ['config']);

app.get('db');
// lines: ["  config 0.0ms", "db 0.1ms"] (dependencies are nested)
```

[Open in the playground](../playground?example=middleware/tracing)

## Fallbacks and dynamic keys

```ts
// Resolve `env.*` keys from the environment when nothing is registered under them.
const env: Record<string, string> = { PORT: '8080' };

const app = new DIContainer().use((key, next) => {
  const value = next();
  if (
    value === undefined &&
    typeof key === 'string' &&
    key.startsWith('env.')
  ) {
    return env[key.slice('env.'.length)];
  }
  return value;
});

// Keys a middleware provides are not in the container's type; declare them where you use them:
const port = app.get('env.PORT' as never) as string; // "8080"
```

Keys a middleware provides are not in the container's type. When the values are known up front, a
registration is simpler and typed. To register services found at startup (files in a directory,
decorated classes), see [Custom registration](./custom-registration.md).

## Keeping renamed keys working

```ts
// Keep an old key working while callers migrate to the new one.
const renamed: Record<string, string> = { mailer: 'emailSender' };

const app = new DIContainer()
  .addSingleton('emailSender', () => ({
    send: (to: string) => `sent to ${to}`,
  }))
  .use((key, next) => {
    const newKey = typeof key === 'string' ? renamed[key] : undefined;
    if (newKey === undefined) return next();
    warnings.push(`"${String(key)}" is deprecated, use "${newKey}"`);
    return next(newKey);
  });

const mailer = app.get('mailer' as never) as { send(to: string): string };
mailer.send('ada@example.com'); // works, and records a deprecation warning
```

## Guards

```ts
// Request-scoped services must never be resolved from the root container.
const root = new DIContainer()
  .addSingleton('config', () => ({ region: 'eu' }))
  .use((key, next, { container }) => {
    if (
      typeof key === 'string' &&
      key.startsWith('request.') &&
      container === root
    ) {
      throw new Error(
        `"${key}" is request-scoped: resolve it from a request fork`,
      );
    }
    return next();
  });

const request = root.fork().addInstance('request.user', { id: 42 });
request.get('request.user'); // ok
// root.get('request.user' as never) would throw
```

## Test doubles

```ts
const app = new DIContainer().addSingleton('clock', () => ({
  now: () => Date.now(),
}));

// A temporary override, removed after the test.
const fakes = new Map<PropertyKey, unknown>([['clock', { now: () => 0 }]]);
const withFakes: Middleware = (key, next) =>
  fakes.has(key) ? fakes.get(key) : next();

app.use(withFakes);
const frozen = app.get('clock').now(); // 0
app.unuse(withFakes);
const real = app.get('clock').now(); // the real clock again
```

To replace a dependency for a whole graph, an [isolated fork](./testing.md) is usually clearer.

## When not to use a middleware

* **Decorating instances** (wrapping a service in a proxy): a middleware runs on every `get()`, so it
  would return a new wrapper each time and break singleton identity. Decorate with a
  [self dependency](./registration.md#decorating-a-service) instead.
* **Observing**: use events.

## Events

```ts
const app = new DIContainer()
  .addEventListener('add', ({ key }) => events.push(`added ${String(key)}`))
  .addEventListener('produce', ({ key }) =>
    events.push(`created ${String(key)}`),
  )
  .addEventListener('dispose', () => events.push('disposed'))
  .addSingleton('db', () => ({ connected: true }));

app.get('db'); // "created db"
app.get('db'); // cached: no event
```

| Event     | When                                          | Payload                                               |
| --------- | --------------------------------------------- | ----------------------------------------------------- |
| `add`     | a registration is added or replaced           | `key`, `replace`, `kind`, `container`                 |
| `replace` | a registration is replaced                    | `key`, `previous` (a `RegistrationInfo`), `container` |
| `reset`   | `reset()` cleared cached instances            | `keys`, `resetParent`, `container`                    |
| `get`     | a service is resolved (also for dependencies) | `key`, `value`, `container`                           |
| `produce` | a factory created an instance                 | `key`, `value`, `container`                           |
| `dispose` | `dispose()` finished                          | `container`                                           |

`container` is a read-only view of the container that emitted the event.
