decorator-dependency-injection
v1.1.0
Published
Lightweight dependency injection (DI) library using native TC39 Stage 3 decorators. Zero dependencies, built-in mocking, TypeScript support.
Maintainers
Readme
Decorator Dependency Injection
A lightweight dependency injection (DI) library for JavaScript and TypeScript using native TC39 Stage 3 decorators.
No reflection. No metadata. No configuration files. Just decorators that work.
Why this library?
- Modern TC39 decorator syntax - no
reflect-metadataoremitDecoratorMetadataneeded - Zero dependencies - tiny bundle size
- Built-in mocking support for unit testing with Jest, Vitest, or Mocha
- Full TypeScript support with type inference
- Works with Node.js, Bun, React, Vue, Svelte, and more
Using a frontend framework? See the Framework Integration Guide for React, Vue, Svelte, SSR, and other environments.
Building a Node.js server? We have Express/Koa/Fastify middleware for automatic request-scoped containers.
Table of Contents
Quick Start
import { Singleton, Inject } from 'decorator-dependency-injection'
@Singleton()
class Database {
query(sql) { return db.execute(sql) }
}
class UserService {
@Inject(Database) db
getUser(id) {
return this.db.query(`SELECT * FROM users WHERE id = ${id}`)
}
}
new UserService().getUser(1) // Database is automatically injectedThat's it. The Database instance is created once and shared everywhere it's injected.
Installation
npm install decorator-dependency-injectionAdd to your .babelrc or babel.config.json:
{
"plugins": [["@babel/plugin-proposal-decorators", { "version": "2023-11" }]]
}Run with Babel:
npx babel-node index.jsFor Jest, add to package.json:
{
"jest": {
"transform": { "^.+\\.jsx?$": "babel-jest" }
}
}See this project's package.json for a complete working example.
Core Concepts
Singleton
A singleton creates one shared instance across your entire application:
import { Singleton, Inject } from 'decorator-dependency-injection'
@Singleton()
class ConfigService {
apiUrl = 'https://api.example.com'
}
class ServiceA {
@Inject(ConfigService) config
}
class ServiceB {
@Inject(ConfigService) config // Same instance as ServiceA
}Factory
A factory creates a new instance each time it's injected:
import { Factory, Inject } from 'decorator-dependency-injection'
@Factory()
class RequestLogger {
id = Math.random()
}
class Handler {
@Inject(RequestLogger) logger // New instance for each Handler
}
new Handler().logger.id !== new Handler().logger.id // trueLazy Injection
By default, dependencies are created when the parent class is instantiated. Use @InjectLazy to defer creation until first access:
import { Singleton, InjectLazy } from 'decorator-dependency-injection'
@Singleton()
class ExpensiveService {
constructor() {
console.log('ExpensiveService created') // Only when accessed
}
}
class MyClass {
@InjectLazy(ExpensiveService) service
doWork() {
this.service.process() // ExpensiveService created here
}
}This is also useful for breaking circular dependencies.
Passing Parameters
Pass constructor arguments after the class reference:
import { Factory, Inject } from 'decorator-dependency-injection'
@Factory()
class Logger {
constructor(prefix, level) {
this.prefix = prefix
this.level = level
}
}
class MyService {
@Inject(Logger, 'MyService', 'debug') logger
}For singletons, parameters are only used on the first instantiation.
Testing
Mocking Dependencies
Use @Mock to replace a dependency with a test double:
import { Singleton, Mock, removeMock, resolve } from 'decorator-dependency-injection'
@Singleton()
class UserService {
getUser(id) { return fetchFromDatabase(id) }
}
// In your test file:
@Mock(UserService)
class MockUserService {
getUser(id) { return { id, name: 'Test User' } }
}
// Now all injections of UserService receive MockUserService
const user = resolve(UserService).getUser(1) // { id: 1, name: 'Test User' }
// Restore the original
removeMock(UserService)Proxy Mocking
Mock only specific methods while keeping the rest of the original implementation:
@Mock(UserService, true) // true enables proxy mode
class PartialMock {
getUser(id) { return { id, name: 'Mocked' } }
// All other methods delegate to the real UserService
}Test Lifecycle
| Function | Purpose |
|----------|---------|
| removeMock(Class) | Remove a specific mock, restore original |
| removeAllMocks() | Remove all mocks, restore all originals |
| resetSingletons() | Clear cached instances (keeps mocks) |
| clearContainer() | Remove all registrations entirely |
import { removeAllMocks, resetSingletons } from 'decorator-dependency-injection'
afterEach(() => {
removeAllMocks() // Restore original implementations
// OR
resetSingletons() // Keep mocks, but get fresh instances
})Note: These functions remove/restore mocks. They do NOT clear mock call history. If using Vitest/Jest spies, call .mockClear() separately.
Testing Best Practices
import { Mock, removeAllMocks, resetSingletons } from 'decorator-dependency-injection'
import { vi, describe, it, beforeEach, afterEach } from 'vitest'
// Hoist mock functions for per-test configuration
const mockGetUser = vi.hoisted(() => vi.fn())
@Mock(UserService)
class MockUserService {
getUser = mockGetUser
}
describe('MyFeature', () => {
beforeEach(() => {
mockGetUser.mockClear() // Clear call history
resetSingletons() // Fresh instances per test
})
afterEach(() => {
removeAllMocks() // Restore originals
})
it('should work', () => {
mockGetUser.mockReturnValue({ id: 1 })
// ... test code ...
expect(mockGetUser).toHaveBeenCalled()
})
})Additional test utilities:
import { isMocked, getMockInstance } from 'decorator-dependency-injection'
// Check if a class is currently mocked
if (isMocked(UserService)) { /* ... */ }
// Access the mock instance to configure it
getMockInstance(UserService).someMethod.mockReturnValue('test')Advanced Features
Private Fields
Both @Inject and @InjectLazy support private fields:
class UserService {
@Inject(Database) #db // Truly private
getUser(id) {
return this.#db.query(`SELECT * FROM users WHERE id = ${id}`)
}
}For lazy injection with private fields, use the accessor keyword:
class UserService {
@InjectLazy(Database) accessor #db // Lazy AND private
}JavaScript doesn't allow Object.defineProperty() on private fields, so @InjectLazy on #field creates the instance at construction time (not truly lazy). The accessor keyword creates a private backing field with getter/setter that enables true lazy behavior.
Static Fields
Inject at the class level (shared across all instances):
class ApiService {
@Inject(Config) static config // Class-level singleton
@Inject(Logger) logger // Instance-level
getUrl() {
return ApiService.config.apiUrl
}
}Named Registrations
Register dependencies under string names instead of class references:
@Singleton('database')
class PostgresDatabase { }
class UserService {
@Inject('database') db
}Manual Resolution
Retrieve instances programmatically (useful for non-class code):
import { resolve } from 'decorator-dependency-injection'
function handleRequest(req) {
const userService = resolve(UserService)
return userService.getUser(req.userId)
}
// With parameters
const logger = resolve(Logger, 'my-module')
// With named registration
const db = resolve('database')Container Introspection
Debug and inspect the container state:
import {
getContainer,
listRegistrations,
isRegistered,
validateRegistrations,
setDebug
} from 'decorator-dependency-injection'
// Check registration status
isRegistered(UserService) // true/false
// Fail fast at startup
validateRegistrations(UserService, AuthService, 'database')
// Throws if any are missing
// List all registrations
listRegistrations().forEach(reg => {
console.log(`${reg.name}: ${reg.type}, mocked: ${reg.isMocked}`)
})
// Enable debug logging
setDebug(true)
// [DI] Registered singleton: UserService
// [DI] Creating singleton: UserService
// [DI] Mocked UserService with MockUserServiceIsolated Containers
Create separate containers for parallel test execution or module isolation:
import { Container } from 'decorator-dependency-injection'
const container = new Container()
container.registerSingleton(MyService)
const instance = container.resolve(MyService)See the Framework Integration Guide for SSR request isolation patterns.
Server Middleware (Express/Koa/Fastify)
For Node.js servers, use the middleware module to get automatic request-scoped containers:
import express from 'express'
import { containerMiddleware, resolve } from 'decorator-dependency-injection/middleware'
const app = express()
app.use(containerMiddleware())
app.get('/user/:id', (req, res) => {
// Each request gets its own isolated container
const userService = resolve(UserService)
res.json(userService.getUser(req.params.id))
})Mixing Global and Request Scopes:
app.get('/data', (req, res) => {
// Use global singleton (e.g., database pool, config)
const db = resolve(DatabasePool, { scope: 'global' })
// Use request-scoped service (default)
const userService = resolve(UserService)
res.json(userService.getData(db))
})See the Framework Integration Guide for Koa, Fastify, and advanced patterns.
API Reference
Decorators
| Decorator | Description |
|-----------|-------------|
| @Singleton(name?) | Register a class as a singleton (example) |
| @Factory(name?) | Register a class as a factory (example) |
| @Inject(target, ...params) | Inject a dependency into a field (example) |
| @InjectLazy(target, ...params) | Inject lazily (on first access) (example) |
| @Mock(target, proxy?) | Replace a dependency with a mock (example) |
Functions
| Function | Description |
|----------|-------------|
| resolve(target, ...params) | Get an instance from the container (example) |
| removeMock(target) | Remove a mock, restore original (example) |
| removeAllMocks() | Remove all mocks (example) |
| resetSingletons(options?) | Clear cached singleton instances (example) |
| clearContainer(options?) | Clear all registrations (example) |
| isRegistered(target) | Check if target is registered (example) |
| isMocked(target) | Check if target is mocked (example) |
| getMockInstance(target) | Get the mock instance (example) |
| validateRegistrations(...targets) | Throw if any target is not registered (example) |
| listRegistrations() | List all registrations (example) |
| getContainer() | Get the default container (example) |
| setDebug(enabled) | Enable/disable debug logging (example) |
| unregister(target) | Remove a registration |
Middleware Functions (/middleware)
| Function | Description |
|----------|-------------|
| containerMiddleware(options?) | Express/Fastify middleware (example) |
| koaContainerMiddleware(options?) | Koa middleware (example) |
| resolve(target, options?) | Get instance from request or global container (example) |
| getContainer() | Get current request container (or global if outside request) |
| getGlobalContainer() | Get the global container |
| runWithContainer(container, fn, options?) | Run function with specific container (example) |
| withContainer(options?) | Wrap handler with container context (example) |
Middleware Options:
| Option | Type | Description |
|--------|------|-------------|
| scope | 'request' \| 'global' | Container scope (default: 'request') |
| debug | boolean | Enable debug logging |
Resolve Options:
| Option | Type | Description |
|--------|------|-------------|
| scope | 'request' \| 'global' | Which container to resolve from (default: 'request') |
| params | any[] | Constructor parameters to pass when creating instance |
TypeScript Support
Full TypeScript definitions are included:
import { Constructor, InjectionToken, RegistrationInfo } from 'decorator-dependency-injection'
// Constructor<T> - a class constructor
const MyClass: Constructor<MyService> = MyService
// InjectionToken<T> - class or string name
const token: InjectionToken<MyService> = MyService
const named: InjectionToken = 'myService'
// RegistrationInfo - from listRegistrations()
// { key, name, type, isMocked, hasInstance }Why Not [Other Library]?
| Feature | This Library | InversifyJS | TSyringe | TypeDI | |---------|--------------|-------------|----------|--------| | Native decorators (Stage 3) | Yes | No (legacy) | No (legacy) | No (legacy) | | Zero dependencies | Yes | No | No | No | | No reflect-metadata | Yes | No | No | No | | Built-in mocking | Yes | No | No | No | | Bundle size | ~3KB | ~50KB | ~15KB | ~20KB |
This library is ideal if you want simple, modern DI without the complexity of container configuration or reflection APIs.
Related Topics
Searching for: JavaScript dependency injection, TypeScript DI container, decorator-based IoC, inversion of control JavaScript, @Inject decorator, @Singleton pattern, service locator pattern, unit test mocking, Jest dependency injection, Vitest mocking.
Version History
- 1.0.0 - Initial release
- 1.0.1 - Automated release with GitHub Actions
- 1.0.2 - Added proxy option to @Mock decorator
- 1.0.3 - Added @InjectLazy decorator
- 1.0.4 - Added Container abstraction, clearContainer(), TypeScript definitions, improved proxy support
- 1.0.5 - Added private field and accessor support for @Inject and @InjectLazy, debug mode, validation helpers
- 1.0.6 - Added resolve() function for non-decorator code
- 1.0.7 - Added more control for mocking in tests and improved compatibility
- 1.1.0 - Added framework integration guide and server middleware
