Skip to content

Dependency Injection

Shokupan provides a powerful, built-in Dependency Injection (DI) system inspired by frameworks like NestJS and Angular. It allows you to manage dependencies efficiently, decouple your code, and easily test your applications.

The system revolves around two main decorators:

  • @Injectable(scope): Marks a class as a Service that can be managed by the container.
  • @Inject(token): Injects a dependency into a class property or constructor parameter.

Mark your class with @Injectable(). By default, services are singletons (shared across the application).

import { Injectable } from 'shokupan';
@Injectable()
export class UserService {
private users = ['Alice', 'Bob'];
getAll() {
return this.users;
}
}

You can inject services directly into your controllers using Constructor Injection.

import { Controller, Get, Injectable } from 'shokupan';
import { UserService } from './user.service';
@Controller('/users')
export class UserController {
// Automatically injected based on type!
constructor(private userService: UserService) {}
@Get('/')
getUsers() {
return this.userService.getAll();
}
}

You can define the lifecycle of your services using the scope parameter in @Injectable.

A single instance is created and reused.

@Injectable('singleton')
// or just @Injectable()
class SharedService { ... }

A new instance is created every time the service is resolved.

@Injectable('instanced')
class RequestIdService {
public id = crypto.randomUUID();
constructor() {
console.log('New instance created:', this.id);
}
}

The cleanest way to declare dependencies.

@Injectable()
class Consumer {
constructor(
private serviceA: ServiceA,
private serviceB: ServiceB
) {}
}

Useful for circular dependencies or optional dependencies.

import { Inject, Injectable } from 'shokupan';
@Injectable()
class Consumer {
@Inject(ServiceA)
private serviceA!: ServiceA;
}

You can also inject services directly into route handlers using the @Use decorator.

import { Get, Use } from 'shokupan';
@Controller('/')
class ApiController {
@Get('/dynamic')
handleRequest(@Use(InstancedService) service: InstancedService) {
return { id: service.id };
}
}

Services can hook into the application lifecycle.

Called immediately after the service is instantiated and its dependencies are resolved.

@Injectable()
class DatabaseService {
onInit() {
console.log('DatabaseService initialized!');
this.connect();
}
}

Called on Singleton services when the application stops (app.stop()). Use this for cleanup (closing connections, stopping timers).

@Injectable()
class DatabaseService {
async onDestroy() {
console.log('Closing database connection...');
await this.disconnect();
}
}

Circular dependencies (A depends on B, B depends on A) in constructors will cause a runtime error:

Error: Circular dependency detected: ServiceA -> ServiceB -> ServiceA

To resolve this, use Property Injection for at least one side of the cycle.

You can access the Container directly if needed, for example in legacy code or testing.

import { Container } from 'shokupan/util/di';
const service = Container.resolve(UserService);