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 |