Lightweight dependency injection (DI) library using native TC39 Stage 3 decorators. Zero dependencies, built-in mocking, TypeScript support.
npm install 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-metadata or emitDecoratorMetadata needed
- 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.
- Quick Start
- Installation
- Core Concepts
- Singleton
- Factory
- Lazy Injection
- Passing Parameters
- Testing
- Mocking Dependencies
- Proxy Mocking
- Test Lifecycle
- Best Practices
- Advanced Features
- Private Fields
- Static Fields
- Named Registrations
- Manual Resolution
- Container Introspection
- Isolated Containers
- Server Middleware
- API Reference
- TypeScript Support
---
``javascript
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 injected
`
That's it. The Database instance is created once and shared everywhere it's injected.
---
`bash`
npm install decorator-dependency-injection
Babel Configuration (required until decorators reach Stage 4)
Add to your .babelrc or babel.config.json:
`json`
{
"plugins": [["@babel/plugin-proposal-decorators", { "version": "2023-11" }]]
}
Run with Babel:
`bash`
npx babel-node index.js
For Jest, add to package.json:`json`
{
"jest": {
"transform": { "^.+\\.jsx?$": "babel-jest" }
}
}
See this project's package.json for a complete working example.
---
A singleton creates one shared instance across your entire application:
`javascript
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
}
`
A factory creates a new instance each time it's injected:
`javascript
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 // true
`
By default, dependencies are created when the parent class is instantiated. Use @InjectLazy to defer creation until first access:
`javascript
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.
Pass constructor arguments after the class reference:
`javascript
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.
---
Use @Mock to replace a dependency with a test double:
`javascript
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)
`
Mock only specific methods while keeping the rest of the original implementation:
`javascript`
@Mock(UserService, true) // true enables proxy mode
class PartialMock {
getUser(id) { return { id, name: 'Mocked' } }
// All other methods delegate to the real UserService
}
| 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 |
`javascript
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.
`javascript
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:
`javascript
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')
`
---
Both @Inject and @InjectLazy support private fields:
`javascript
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:
`javascript`
class UserService {
@InjectLazy(Database) accessor #db // Lazy AND private
}
Why accessor for lazy private fields?
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.
Inject at the class level (shared across all instances):
`javascript
class ApiService {
@Inject(Config) static config // Class-level singleton
@Inject(Logger) logger // Instance-level
getUrl() {
return ApiService.config.apiUrl
}
}
`
Register dependencies under string names instead of class references:
`javascript
@Singleton('database')
class PostgresDatabase { }
class UserService {
@Inject('database') db
}
`
Retrieve instances programmatically (useful for non-class code):
`javascript
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')
`
Debug and inspect the container state:
`javascript
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 MockUserService
`
Create separate containers for parallel test execution or module isolation:
`javascript
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.
For Node.js servers, use the middleware module to get automatic request-scoped containers:
`javascript
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:
`javascript`
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.
---
| 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) |
| 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 |
| 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 |
---
Full TypeScript definitions are included:
`typescript
import { Constructor, InjectionToken, RegistrationInfo } from 'decorator-dependency-injection'
// Constructor
const MyClass: Constructor
// InjectionToken
const token: InjectionToken
const named: InjectionToken = 'myService'
// RegistrationInfo - from listRegistrations()
// { key, name, type, isMocked, hasInstance }
``
---
| 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.
---
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.
---
- 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