@ntrg/simple-di
v1.0.2
Published
Lightweight dependency injection container for TypeScript
Maintainers
Readme
simple-di
A lightweight yet powerful Dependency Injection container for TypeScript and Node.js applications.
Supports:
- Constructor injection
- Decorators (
@Injectable,@Inject,@Optional) - Singleton / Transient / Scoped lifetimes
- Lazy injection
- Factory providers
- Value providers
- Multi providers
- Circular dependency detection
- Zero framework lock-in
Features
- Minimal and easy-to-understand API
- Built with TypeScript
- Uses
reflect-metadata - Inspired by DI systems from ASP.NET Core, NestJS, and Angular
- Works in Node.js, Express, React SSR, CLI apps, and more
- Tiny and framework agnostic
Installation
npm install @ntrg/simple-di reflect-metadataor
yarn add @ntrg/simple-di reflect-metadataor
pnpm add @ntrg/simple-di reflect-metadataEnable Decorators
tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}Import Reflect Metadata
You must import reflect-metadata once at application startup.
import 'reflect-metadata';Quick Start
import 'reflect-metadata';
import { Container, Injectable } from '@ntrg/simple-di';
@Injectable()
class Logger {
log(message: string) {
console.log(message);
}
}
@Injectable()
class UserService {
constructor(private logger: Logger) {}
getUsers() {
this.logger.log('Fetching users...');
}
}
const container = new Container();
container.register({
provide: Logger,
useClass: Logger,
});
container.register({
provide: UserService,
useClass: UserService,
});
const service = container.resolve(UserService);
service.getUsers();Core Concepts
Providers
A provider describes how a dependency should be created.
Class Provider
container.register({
provide: UserService,
useClass: UserService,
});Factory Provider
container.register({
provide: 'API_URL',
useFactory: () => {
return process.env.API_URL;
},
});Value Provider
container.register({
provide: 'APP_NAME',
useValue: 'Simple DI',
});Injection
Constructor Injection
@Injectable()
class Logger {}
@Injectable()
class UserService {
constructor(private logger: Logger) {}
}Custom Tokens
Useful for interfaces or primitive values.
const CONFIG = Symbol('CONFIG');
container.register({
provide: CONFIG,
useValue: {
port: 3000,
},
});
@Injectable()
class AppService {
constructor(
@Inject(CONFIG)
private config: any,
) {}
}String Tokens
container.register({
provide: 'API_URL',
useValue: 'https://api.example.com',
});
@Injectable()
class ApiService {
constructor(
@Inject('API_URL')
private apiUrl: string,
) {}
}Scopes
Singleton (Default)
Only one instance is created.
container.register({
provide: Logger,
useClass: Logger,
scope: 'singleton',
});Transient
Creates a new instance every resolution.
container.register({
provide: Logger,
useClass: Logger,
scope: 'transient',
});Scoped
Creates one instance per scope/container.
container.register({
provide: RequestContext,
useClass: RequestContext,
scope: 'scoped',
});Example:
const root = new Container();
const request1 = root.createScope();
const request2 = root.createScope();
const ctx1 = request1.resolve(RequestContext);
const ctx2 = request2.resolve(RequestContext);
console.log(ctx1 === ctx2); // falseOptional Dependencies
Use @Optional() when a dependency may not exist.
@Injectable()
class CacheService {}
@Injectable()
class UserService {
constructor(
@Optional()
private cache?: CacheService,
) {}
}Lazy Injection
Lazy injection helps avoid circular dependency issues and improves startup performance.
@Injectable()
class A {
constructor(
@Inject(() => B)
private b: B,
) {}
}
@Injectable()
class B {
constructor(private a: A) {}
}Multi Providers
Register multiple providers under the same token.
const HOOKS = Symbol('HOOKS');
container.register({
provide: HOOKS,
useValue: 'hook-1',
multi: true,
});
container.register({
provide: HOOKS,
useValue: 'hook-2',
multi: true,
});
const hooks = container.resolve(HOOKS);
console.log(hooks);
// ['hook-1', 'hook-2']Circular Dependency Detection
The container automatically detects circular dependencies.
@Injectable()
class A {
constructor(private b: B) {}
}
@Injectable()
class B {
constructor(private a: A) {}
}Resolving A will throw an error like:
Circular dependency detected:
A -> B -> AAPI Reference
Container
register(provider)
Registers a provider.
container.register({
provide: Logger,
useClass: Logger,
});resolve(token)
Synchronously resolves a dependency.
const logger = container.resolve(Logger);createScope()
Creates a child scoped container.
const scoped = container.createScope();Decorators
@Injectable()
Marks a class as injectable.
@Injectable()
class UserService {}@Inject(token)
Injects a custom token.
constructor(
@Inject('API_URL')
private url: string
) {}@Optional()
Marks a dependency as optional.
constructor(
@Optional()
private cache?: CacheService
) {}Types
type Scope = 'singleton' | 'transient' | 'scoped';type Token<T = any> = Constructor<T> | symbol | string;Example: Express.js
import express from 'express';
import 'reflect-metadata';
import { Container, Injectable } from '@ntrg/simple-di;
@Injectable()
class UserRepository {
findAll() {
return ['John', 'Jane'];
}
}
@Injectable()
class UserController {
constructor(private repo: UserRepository) {}
getUsers(req, res) {
res.json(this.repo.findAll());
}
}
const container = new Container();
container.register({
provide: UserRepository,
useClass: UserRepository,
});
container.register({
provide: UserController,
useClass: UserController,
});
const app = express();
app.get('/users', (req, res) => {
const controller = container.resolve(UserController);
controller.getUsers(req, res);
});
app.listen(3000);Best Practices
Use Symbols for Shared Tokens
export const DATABASE = Symbol('DATABASE');Keep Business Logic Framework Independent
Dependency Injection helps separate business logic from infrastructure.
Prefer Constructor Injection
Constructor injection makes dependencies explicit and easier to test.
Testing Example
class FakeLogger {
log() {}
}
const container = new Container();
container.register({
provide: Logger,
useClass: FakeLogger,
});
container.register({
provide: UserService,
useClass: UserService,
});
const service = container.resolve(UserService);Roadmap
Planned features:
- Async factories
- Property injection
- Module system
- Lifecycle hooks
- Auto registration
- Request pipeline support
- Devtools integration
Requirements
- Node.js >= 16
- TypeScript >= 6
License
MIT
Contributing
Contributions, issues, and feature requests are welcome.
If you find a bug or have an idea for improvement, feel free to open an issue or submit a pull request.
Author
Built with TypeScript for developers who want a clean and simple DI experience.
