npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

Readme

Decorator Dependency Injection

npm version npm downloads Build Status Coverage License

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.

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 injected

That's it. The Database instance is created once and shared everywhere it's injected.


Installation

npm install decorator-dependency-injection

Add to your .babelrc or babel.config.json:

{
  "plugins": [["@babel/plugin-proposal-decorators", { "version": "2023-11" }]]
}

Run with Babel:

npx babel-node index.js

For 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  // true

Lazy 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 MockUserService

Isolated 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