Careeroza
How DI Works Under the Hood
Build a Tiny DI Container
NestJS reads constructor types from metadata. Our runnable example has no reflect-metadata, so we pass the tokens explicitly with an @Inject() parameter decorator (NestJS also has a real @Inject() for exactly this case). The container creates each class once and reuses it.
Example
A mini NestJS-style container with legacy decorators:
type Ctor<T = unknown> = new (...args: any[]) => T;
const injectable = new Set<Ctor>();
const paramTokens = new Map<Ctor, Ctor[]>();
function Injectable(): ClassDecorator {
return (target) => { injectable.add(target as unknown as Ctor); };
}
function Inject(token: Ctor): ParameterDecorator {
return (target, _key, index) => {
const list = paramTokens.get(target as unknown as Ctor) ?? [];
list[index] = token;
paramTokens.set(target as unknown as Ctor, list);
};
}
class Container {
private instances = new Map<Ctor, unknown>();
get<T>(cls: Ctor<T>): T {
if (this.instances.has(cls)) return this.instances.get(cls) as T;
if (!injectable.has(cls)) throw new Error(cls.name + ' is not @Injectable');
const deps = (paramTokens.get(cls) ?? []).map((t) => this.get(t));
console.log('creating', cls.name, 'with', deps.map((d) => (d as object).constructor.name));
const instance = new cls(...deps);
this.instances.set(cls, instance);
return instance;
}
}
@Injectable()
class PricingService {
withGst(p: number) { return Math.round(p * 1.18); }
}
@Injectable()
class ProductsRepository {
findPrice(id: number) { return id === 7 ? 1299 : 999; }
}
@Injectable()
class ProductsService {
constructor(
@Inject(ProductsRepository) private readonly repo: ProductsRepository,
@Inject(PricingService) private readonly pricing: PricingService,
) {}
finalPrice(id: number) { return this.pricing.withGst(this.repo.findPrice(id)); }
}
const app = new Container();
const service = app.get(ProductsService);
console.log('final price of 7:', service.finalPrice(7));
console.log('same instance again?', app.get(ProductsService) === service);Output
creating ProductsRepository with [] creating PricingService with [] creating ProductsService with [ 'ProductsRepository', 'PricingService' ] final price of 7: 1533 same instance again? true
What to Notice
- The container resolves dependencies first (repository and pricing), then the service that needs them.
- Each class is created once and cached: that is the default singleton scope in NestJS.
- The service never calls
new: you can give it a fake repository in tests.