Documents injecute 1.x · Migrating from 0.x
Skip to content

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 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

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.

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 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 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
EventWhenPayload
adda registration is added or replacedkey, replace, kind, container
replacea registration is replacedkey, previous (a RegistrationInfo), container
resetreset() cleared cached instanceskeys, resetParent, container
geta service is resolved (also for dependencies)key, value, container
producea factory created an instancekey, value, container
disposedispose() finishedcontainer

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

Released under the MIT License.