Dependency Injection

Katal ships an IoC container that is available on every Application instance. It supports singleton bindings, transient (per-resolve) bindings, and pre-created instances.


Concepts

Concept Lifespan Use when
Singleton One shared instance per container Stateful services — DB connections, cache, config
Transient New instance per resolve() call Stateless utilities — mailers, formatters
Instance Pre-created value (treated as singleton) Third-party objects, test doubles

Registering Services

singleton(name, factory)

The factory is called once. Every resolve() returns the same object.

app.singleton("db", () => new DatabaseClient({
  url: process.env.DATABASE_URL!,
}));

bind(name, factory)

The factory is called on every resolve().

app.bind("auditor", () => new AuditLogger());

instance(name, value)

Register an already-created object.

const redis = new Redis({ host: "localhost" });
app.instance("redis", redis);

Resolving Services

const db    = app.resolve<DatabaseClient>("db");
const redis = app.resolve<Redis>("redis");

Throws if the name is not registered:

Error: Service "db" not found in container

Check Existence

if (app.container.has("redis")) {
  const r = app.resolve<Redis>("redis");
}

Forget a Binding

Useful in tests to replace a service:

app.container.forget("db");
app.singleton("db", () => fakeDatabase);

Accessing the Raw Container

The underlying Container instance is accessible for advanced use:

import { Container } from "katal";

const container = app.resolve<Container>("container");
// or just use app.singleton / app.bind / app.resolve

Service Providers

For larger applications, organise registrations with Service Providers:

import { ServiceProvider } from "katal";

class DatabaseProvider extends ServiceProvider {
  register() {
    this.app.singleton("db", () => new DatabaseClient({
      url: process.env.DATABASE_URL!,
    }));
    this.app.singleton("redis", () => new Redis());
  }

  async boot() {
    const db = this.app.resolve<DatabaseClient>("db");
    await db.connect();
  }
}

app.registerProvider(new DatabaseProvider());

Using Services in Controllers

Resolve from the app instance before registering routes, or inject via the request context.

const db = app.resolve<DatabaseClient>("db");

class GetUserController extends Controller {
  async handle(ctx: RequestContext) {
    const user = await db.findById(ctx.params.id);
    if (!user) return this.error("Not found", 404);
    return this.success(user);
  }
}

Testing with the Container

Swap services with fakes in tests:

import { AppFactory } from "katal/testing";

const factory = await AppFactory.create(() => {
  const app = new Application({ port: 0 });
  app.singleton("db", () => new FakeDatabase());
  return app;
});

Built-in Registrations

Katal pre-registers these names in every container:

Name Value
"router" The Router instance
"config" The raw AppConfig object
"options" The ConfigManager instance
"health" The HealthRegistry instance
"events" The EventBus instance
"cache" The active CacheStore
"rateLimitStore" The active RateLimitStore
"queue" Lazy QueueStore singleton
"authorization" The Authorization instance

See Also