IoC Container#
Built-in dependency injection container with singleton/transient scopes, multiple provider types, and circular dependency detection.
Quick Start#
typescript
import { Buntok, Container, Dependencies, Controller, Get } from "@buntok/core";
import type { Context } from "@buntok/core";
// 1. Define services with @Dependencies
class UserRepository {
findAll() { return [{ id: 1, name: "John" }]; }
}
@Dependencies(UserRepository)
class UserService {
constructor(private repo: UserRepository) {}
getUsers() { return this.repo.findAll(); }
}
// 2. Controller with @Dependencies
@Dependencies(UserService)
@Controller("/users")
class UserController {
constructor(private service: UserService) {}
@Get("/")
list(ctx: Context) { return ctx.json(this.service.getUsers()); }
}
// 3. One line resolves the entire tree
const app = new Buntok();
const container = new Container();
container.scan([UserController]); // auto-registers all 3
app.setContainer(container);
app.registerController(UserController);
app.listen(1212);Info
container.scan() reads tokens from @Dependencies() and registers factory providers bottom-up. Controllers without @Dependencies don't need to be in scan() - registerController() creates them directly.Providers#
| Provider | Description |
|---|---|
| useClass | Create instance from class constructor |
| useValue | Return a static value (always singleton) |
| useFactory | Create instance from a function (receives container) |
ClassProvider#
typescript
container.register(UserService, {
useClass: UserService,
scope: "singleton", // default
});
// Or auto-register by class constructor
container.registerClass(UserService);ValueProvider#
typescript
container.register("DATABASE_URL", {
useValue: process.env.DATABASE_URL,
});
// Use string tokens for config values
const url = container.resolve<string>("DATABASE_URL");FactoryProvider#
typescript
container.register("CacheDriver", {
useFactory: (container) => {
const config = container.resolve("Config");
return new RedisCache(config.redisUrl);
},
scope: "singleton",
});@Dependencies Decorator#
Declare constructor dependencies explicitly for container.scan(). Works with TC39 decorators - no emitDecoratorMetadata or reflect-metadata needed.
typescript
import { Dependencies } from "@buntok/core";
@Dependencies(UserRepository)
class UserService {
constructor(private repo: UserRepository) {}
}
@Dependencies(UserService, Logger)
class UserController {
constructor(private service: UserService, private logger: Logger) {}
}
// Empty @Dependencies = no constructor params
@Dependencies()
class HealthController {
health() { return { ok: true }; }
}Auto-scan (Recommended)#
Use container.scan() to auto-resolve the full dependency tree. Only classes with @Dependencies need to be passed to scan().
typescript
const container = new Container();
// scan() auto-registers all dependencies transitively
container.scan([OrderController]);
// Resolves: OrderController → OrderService → OrderRepo + PaymentGateway
app.setContainer(container);
// Controllers WITHOUT @Dependencies don't need scan()
// registerController() creates them directly
app.registerController(OrderController);
app.registerController(HealthController); // no DI, works fineInfo
container.scan() is synchronous. Only controllers with @Dependencies() need to be in the scan list. Controllers without constructor dependencies are created directly by registerController().Scopes#
| Scope | Behavior |
|---|---|
| singleton | One instance, cached and reused (default) |
| transient | New instance every time it's resolved |
typescript
// Singleton - same instance returned every time
container.register(UserService, { useClass: UserService, scope: "singleton" });
// Transient - new instance each time
container.register(RequestLogger, { useClass: RequestLogger, scope: "transient" });Warning
@Injectable & @Inject have been removed. Use @Dependencies + container.scan() instead (see Quick Start above). Field decorators are no longer supported.Container API#
| Method | Description |
|---|---|
| container.register(token, provider) | Register a provider for a token |
| container.registerClass(cls, scope?) | Auto-register class by constructor |
| container.scan(classes[], scope?) | Auto-scan classes via @Dependencies() and register with dependency resolution |
| container.resolve<T>(token) | Resolve and return instance (throws if not found) |
| container.get<T>(token) | Resolve or return undefined |
| container.has(token) | Check if token is registered |
| container.hasResolved(token) | Check if token is already cached |
| container.clear() | Clear all providers and cache |
Circular Dependency Detection#
Warning
The container detects circular dependencies and throws an error with the class name to help you debug.
Full Example: Controller with DI#
typescript
import { Buntok, Container, Dependencies, Controller, Get } from "@buntok/core";
import type { Context } from "@buntok/core";
class UserRepository {
async findAll() {
return await db.user.findMany();
}
async findById(id: string) {
return await db.user.findUnique({ where: { id } });
}
}
@Dependencies(UserRepository)
class UserService {
constructor(private repo: UserRepository) {}
getUsers() { return this.repo.findAll(); }
getUser(id: string) { return this.repo.findById(id); }
}
@Dependencies(UserService)
@Controller("/users")
class UserController {
constructor(private service: UserService) {}
@Get("/")
async list(ctx: Context) {
const users = await this.service.getUsers();
return ctx.json(users);
}
}
// One line resolves everything
const app = new Buntok();
const container = new Container();
container.scan([UserController]); // auto-registers UserRepository → UserService → UserController
app.setContainer(container);
app.registerController(UserController);
app.listen(1212);