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#

ProviderDescription
useClassCreate instance from class constructor
useValueReturn a static value (always singleton)
useFactoryCreate 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 }; }
}

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 fine
Info
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#

ScopeBehavior
singletonOne instance, cached and reused (default)
transientNew 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#

MethodDescription
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);