README.md

June 25, 2026 ยท View on GitHub

Node Dependency Injection

Node Dependency Injection

Modern TypeScript DI without decorators and without reflect-metadata.
Autowiring, validation, and container compilation for maintainable Node.js services.
Inspired by the battle-tested Symfony DI container.
node-di.dev

Npm Version Build Status Code Coverage Npm Downloads Known Vulnerabilities License


Why Node Dependency Injection?

Managing dependencies manually leads to tightly coupled, hard-to-test code. Node Dependency Injection gives you a powerful, flexible IoC container that wires your application together โ€” keeping your classes clean, your tests simple, and your architecture solid.

If you are comparing options in 2026, the core value is simple: TypeScript autowiring by class, without decorators, without runtime metadata hacks, with compile-time style validation workflows.


โœจ Features

๐Ÿ”„ Autowire โ€” zero-config DI for TypeScript๐Ÿ“ Config files โ€” YAML, JSON or JS
๐Ÿญ Factory pattern โ€” flexible service creation๐Ÿท๏ธ Service tagging โ€” group & inject by tag
๐Ÿ’ค Lazy services โ€” instantiate only when needed๐ŸŽจ Decorators โ€” wrap services transparently
โšก Compiler passes โ€” transform the container at build time๐Ÿ”’ Private services โ€” encapsulate internals
๐ŸŒณ Parent / Abstract services โ€” share config via inheritance๐Ÿงฉ Non-shared services โ€” new instance per call
๐ŸŒฟ Environment parameters โ€” %env(VAR)% support๐Ÿ—‘๏ธ Deprecation warnings โ€” mark services as deprecated
๐Ÿ“ฆ Express middleware โ€” first-class web framework support๐Ÿ–ฅ๏ธ CLI โ€” inspect & validate your container
๐Ÿ”€ Conditional services โ€” register services based on env or other services๐Ÿ—๏ธ Keyed services โ€” named strategy pattern with registerKeyed
๐Ÿ”— Autowire + Keyed โ€” inject keyed services via parameter binds

๐Ÿ“Š Why choose NDI over Awilix, InversifyJS or tsyringe?

The goal is not to attack alternatives. This table explains the concrete trade-offs and where NDI is opinionated.

Featurenode-dependency-injectionInversifyJStsyringeAwilix
Config files (YAML / JSON / JS)โœ… Nativeโš ๏ธ Programmatic-firstโš ๏ธ Programmatic-firstโš ๏ธ Programmatic-first
TypeScript autowire without decoratorsโœ… NativeโŒโŒโŒ
Keyed services (named strategy groups)โœ… Nativeโš ๏ธ Via custom patternsโš ๏ธ Via tokens/patternsโš ๏ธ Via aliases/patterns
Conditional service registrationโœ… Nativeโš ๏ธ Manual logicโš ๏ธ Manual logicโš ๏ธ Manual logic
Compiler passes / compile-time transformsโœ… NativeโŒโŒโŒ
Service tags and tagged lookupโœ… Nativeโš ๏ธ Manual metadata patternsโš ๏ธ Manual token patternsโš ๏ธ Naming/registration patterns
Private servicesโœ… Nativeโš ๏ธ Container conventionsโš ๏ธ Container conventionsโš ๏ธ Container conventions
CLI container inspection / validationโœ… Native (ndi)โŒโŒโŒ
Parent / abstract definitionsโœ… Nativeโš ๏ธ Composition-basedโš ๏ธ Composition-basedโš ๏ธ Composition-based
Lazy servicesโœ… Nativeโœ…โš ๏ธ Partial patternsโš ๏ธ Partial patterns

Legend: โœ… built-in, โš ๏ธ possible with custom conventions or extra setup, โŒ not available as a first-class feature.


๐Ÿš€ Installation

Website: node-di.dev

npm install --save node-dependency-injection

๐Ÿ Quick Start

Start with class-based autowiring (the default recommendation):

import { Autowire, ContainerBuilder } from 'node-dependency-injection'
import UserService from '@src/service/UserService'

const container = new ContainerBuilder(false, '/path/to/src')
const autowire = new Autowire(container)

await autowire.process()
await container.compile()

const userService = container.get(UserService)

No decorators. No tokens. No reflect-metadata.

Prefer explicit IDs and programmatic registration? You can still do that:

import { ContainerBuilder } from 'node-dependency-injection'
import Mailer from './services/Mailer'
import ExampleService from './services/ExampleService'

const container = new ContainerBuilder()

container.register('service.example', ExampleService)
container.register('service.mailer', Mailer).addArgument('service.example')

await container.compile()

const mailer = container.get('service.mailer')

๐Ÿ”„ Autowire (TypeScript)

Zero-configuration dependency injection โ€” NDI reads your TypeScript type annotations and wires everything automatically:

import { ContainerBuilder, Autowire } from 'node-dependency-injection'

const container = new ContainerBuilder(false, '/path/to/src')
const autowire = new Autowire(container)
await autowire.process()
await container.compile()

// Retrieve by class โ€” no string IDs needed
import SomeService from '@src/service/SomeService'
const service = container.get(SomeService)

// Or retrieve by ID with an explicit type
const typedService = container.get<SomeService>('service.some')

Production tip: dump the autowired config to a YAML file and load it directly in prod โ€” no TypeScript scanning overhead.

if (process.env.NODE_ENV === 'development') {
  const autowire = new Autowire(container)
  autowire.serviceFile = new ServiceFile('/dist/services.yaml')
  await autowire.process()
} else {
  const loader = new YamlFileLoader(container)
  await loader.load('/dist/services.yaml')
}
await container.compile()

The generated services.yaml uses human-readable service IDs derived from the file path relative to your source root (e.g. Service/Mailer), making it easy to review, audit, and debug your application's service graph:

# services.yaml (human-readable โ€” default since v4.0)
services:
  Service/Mailer:
    class: /Service/Mailer
    arguments:
      - '@Service/Transport'
  Service/Transport:
    class: /Service/Transport
    arguments: []

Service ID strategy

StrategyDefaultService ID exampleDescription
readableโœ… v4.0+Service/MailerPath relative to defaultDir, extension stripped
legacyv3.xU2VydmljZS9NYWlsZXI=Base64-encoded absolute path

To opt back in to the legacy strategy (e.g. for gradual migration):

const autowire = new Autowire(container)
autowire.makeIdLegacy() // switch back to base64-encoded IDs

๐Ÿ—๏ธ Keyed Services

Keyed services let you register multiple implementations of the same interface under a named group, then retrieve a specific one by key or inject the entire group as a Map.

Programmatic API

import { ContainerBuilder, KeyedReference, KeyedGroupReference } from 'node-dependency-injection'
import StripePayment from './payments/StripePayment'
import PaypalPayment from './payments/PaypalPayment'
import CheckoutService from './CheckoutService'
import PaymentRouter from './PaymentRouter'

const container = new ContainerBuilder()

// Register implementations under the 'payment' group
container.registerKeyed('payment', 'stripe', StripePayment).setDefault(true)
container.registerKeyed('payment', 'paypal', PaypalPayment)

// Inject a specific key
container.register('checkout', CheckoutService)
  .addArgument(new KeyedReference('payment', 'stripe'))

// Inject the full group as a Map<key, instance>
container.register('payment.router', PaymentRouter)
  .addArgument(new KeyedGroupReference('payment'))

// Retrieve by key or get the default
const stripe = container.getKeyed('payment', 'stripe')
const defaultPayment = container.getKeyed('payment')          // returns the .setDefault(true) one
const allPayments = container.getKeyedGroup('payment')        // Map { 'stripe' => โ€ฆ, 'paypal' => โ€ฆ }

YAML configuration

services:
  payment.stripe:
    class: 'payments/StripePayment'
    keyed:
      group: payment
      key: stripe
      default: true

  payment.paypal:
    class: 'payments/PaypalPayment'
    keyed:
      group: payment
      key: paypal

  checkout:
    class: 'CheckoutService'
    arguments: ['@keyed(payment, stripe)']

  payment.router:
    class: 'PaymentRouter'
    arguments: ['@keyed_group(payment)']

Autowire integration

When using Autowire you can inject keyed services into typed constructor parameters by registering a bind whose name matches the parameter name:

// TypeScript service
export default class CheckoutService {
  constructor(private readonly payment: IPaymentService) {}
}

export default class PaymentRouter {
  constructor(private readonly payments: Map<string, IPaymentService>) {}
}
container.registerKeyed('payment', 'stripe', StripePaymentService)
container.registerKeyed('payment', 'paypal', PaypalPaymentService)

// The bind name must match the constructor parameter name exactly
container.addBind('payment', new KeyedReference('payment', 'stripe'))
container.addBind('payments', new KeyedGroupReference('payment'))

const autowire = new Autowire(container)
await autowire.process()
await container.compile()

// container.get(CheckoutService).payment  โ†’ StripePaymentService
// container.get(PaymentRouter).payments   โ†’ Map { 'stripe' => โ€ฆ, 'paypal' => โ€ฆ }

Note: binds take priority over type-based resolution. The same mechanism works for scalar values โ€” container.addBind('apiKey', '%env(API_KEY)%') โ€” so keyed service binds fit naturally into the existing bind API.


๐Ÿ“ Configuration Files

Prefer declarative config? Use YAML, JSON or JS:

# services.yaml
services:
  service.example:
    class: 'services/ExampleService'

  service.mailer:
    class: 'services/Mailer'
    arguments: ['@service.example']
import { ContainerBuilder, YamlFileLoader } from 'node-dependency-injection'

const container = new ContainerBuilder()
const loader = new YamlFileLoader(container)
await loader.load('/path/to/services.yaml')
await container.compile()

const mailer = container.get('service.mailer')

๐Ÿ”€ Conditional Services

Register services only when specific conditions are met โ€” evaluated at compile() time.

import { ContainerBuilder, Condition } from 'node-dependency-injection'

const container = new ContainerBuilder()

// Register only when an environment variable is set
container.register('cache.redis', RedisCache)
  .setCondition(Condition.envExists('REDIS_URL'))

// Fallback: register only when another service was NOT registered (TryAdd)
container.register('cache.memory', InMemoryCache)
  .whenMissing('cache.redis')

// Register only when a sibling service IS registered
container.register('metrics', Prometheus)
  .whenServiceExists('http.server')

// Combine conditions
container.register('feature.x', FeatureX)
  .setCondition(Condition.all(
    Condition.envEquals('NODE_ENV', 'production'),
    Condition.envExists('FEATURE_X_ENABLED')
  ))

await container.compile()

The same conditions are available declaratively in YAML:

services:
  cache.redis:
    class: 'services/cache/RedisCache'
    when:
      env_exists: REDIS_URL

  cache.memory:
    class: 'services/cache/InMemoryCache'
    when:
      missing: cache.redis

  metrics.prometheus:
    class: 'services/metrics/Prometheus'
    when:
      service_exists: http.server

  logger.verbose:
    class: 'services/logger/VerboseLogger'
    when:
      env_equals: { var: LOG_LEVEL, value: debug }

See the Conditional Services wiki page for the full API reference and advanced usage.


๐Ÿ“ฆ Ecosystem

Express Middleware

Use NDI seamlessly with Express โ€” retrieve the container directly from any request:

npm install --save node-dependency-injection-express-middleware
import NDIMiddleware from 'node-dependency-injection-express-middleware'
import express from 'express'

const app = express()
app.use(new NDIMiddleware({ serviceFilePath: 'services.yaml' }).middleware())

Express Middleware Documentation

CLI

Inspect and validate your container from the command line:

# Validate a config file
ndi config:check /path/to/services.yaml

# Inspect a specific service
ndi container:service /path/to/services.yaml service.mailer

๐Ÿ“– Documentation

The full documentation lives in the project wiki, including guides on:


๐Ÿค Contributing

Contributions are welcome! Please read the contribution guide before submitting a pull request.


๐Ÿ™ Credits

Inspired by the Symfony Dependency Injection component โ€” a special thanks to the Symfony team for their outstanding work.


MIT License ย ยทย  Made with โค๏ธ by @zazoomauro