Skip to main content

InversifyJS vs Awilix vs TSyringe: Dependency Injection in Node.js (2026)

·PkgPulse Team

TL;DR

InversifyJS is the most popular decorator-based DI container — @injectable, @inject, and a familiar pattern for developers coming from NestJS or Spring. Awilix takes the opposite approach — no decorators, no reflect-metadata, just a functional container that auto-wires by naming convention. TSyringe is Microsoft's lightweight DI container — similar to InversifyJS but simpler, with lower boilerplate. For most Node.js apps: Awilix (no decorators, simpler). For teams comfortable with NestJS-style decorators: InversifyJS or TSyringe. And honestly: if you're using NestJS, just use NestJS's built-in DI.

Key Takeaways

  • inversify: ~1.5M weekly downloads — decorator-based, @injectable/@inject, most popular IoC container
  • awilix: ~400K weekly downloads — functional, no decorators, auto-wire by naming, simpler
  • tsyringe: ~600K weekly downloads — Microsoft, lighter than InversifyJS, same decorator pattern
  • All require experimentalDecorators: true and emitDecoratorMetadata: true — except Awilix
  • Dependency injection is most useful for: testability, swapping implementations, large applications
  • For small apps, DI containers add complexity without benefit — just pass dependencies manually

PackageWeekly DownloadsDecoratorsreflect-metadataAuto-wire
inversify~1.5M✅ Required
awilix~400K
tsyringe~600K✅ Required

InversifyJS

InversifyJS — the most popular IoC container for TypeScript:

Setup

// tsconfig.json — required for InversifyJS:
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

// main.ts — import reflect-metadata FIRST:
import "reflect-metadata"

Define interfaces and implementations

import "reflect-metadata"
import { injectable, inject, Container } from "inversify"

// Symbols as identifiers (recommended over string tokens):
const TYPES = {
  PackageRepository: Symbol.for("PackageRepository"),
  CacheService: Symbol.for("CacheService"),
  PackageService: Symbol.for("PackageService"),
} as const

// Interface:
interface IPackageRepository {
  findByName(name: string): Promise<Package | null>
  getTopPackages(limit: number): Promise<Package[]>
}

interface ICacheService {
  get<T>(key: string): Promise<T | null>
  set<T>(key: string, value: T, ttl: number): Promise<void>
}

// Implementations:
@injectable()
class NpmPackageRepository implements IPackageRepository {
  async findByName(name: string): Promise<Package | null> {
    const res = await fetch(`https://registry.npmjs.org/${name}`)
    if (!res.ok) return null
    return res.json()
  }

  async getTopPackages(limit: number): Promise<Package[]> {
    // Implementation...
    return []
  }
}

@injectable()
class RedisCache implements ICacheService {
  async get<T>(key: string): Promise<T | null> {
    // Redis get...
    return null
  }

  async set<T>(key: string, value: T, ttl: number): Promise<void> {
    // Redis set...
  }
}

// Service with injected dependencies:
@injectable()
class PackageService {
  constructor(
    @inject(TYPES.PackageRepository) private repo: IPackageRepository,
    @inject(TYPES.CacheService) private cache: ICacheService
  ) {}

  async getPackageHealth(name: string): Promise<PackageHealth> {
    const cached = await this.cache.get<PackageHealth>(`health:${name}`)
    if (cached) return cached

    const pkg = await this.repo.findByName(name)
    if (!pkg) throw new Error(`Package ${name} not found`)

    const health = calculateHealth(pkg)
    await this.cache.set(`health:${name}`, health, 3600)
    return health
  }
}

Container setup and resolution

import { Container } from "inversify"

// Configure container:
const container = new Container()

container.bind<IPackageRepository>(TYPES.PackageRepository)
  .to(NpmPackageRepository)
  .inSingletonScope()  // One instance shared across all consumers

container.bind<ICacheService>(TYPES.CacheService)
  .to(RedisCache)
  .inSingletonScope()

container.bind<PackageService>(TYPES.PackageService)
  .to(PackageService)
  .inSingletonScope()

// Resolve:
const packageService = container.get<PackageService>(TYPES.PackageService)
// Dependencies injected automatically

const health = await packageService.getPackageHealth("react")

Testing with InversifyJS

import { Container } from "inversify"

// Swap implementations for tests:
const testContainer = new Container()

// Mock repository:
const mockRepo: IPackageRepository = {
  findByName: vi.fn().mockResolvedValue({ name: "react", version: "18.3.1" }),
  getTopPackages: vi.fn().mockResolvedValue([]),
}

testContainer.bind<IPackageRepository>(TYPES.PackageRepository)
  .toConstantValue(mockRepo)

testContainer.bind<ICacheService>(TYPES.CacheService)
  .toConstantValue({ get: vi.fn().mockResolvedValue(null), set: vi.fn() })

testContainer.bind<PackageService>(TYPES.PackageService).to(PackageService)

const service = testContainer.get<PackageService>(TYPES.PackageService)
// service uses mock implementations

Awilix

Awilix — functional DI without decorators:

Setup (no reflect-metadata needed)

import { createContainer, asClass, asValue, asFunction, Lifetime } from "awilix"

// No tsconfig changes needed — no decorators

Register and resolve

import { createContainer, asClass, asValue, Lifetime } from "awilix"

// Classes — dependencies injected by parameter name matching:
class PackageRepository {
  async findByName(name: string): Promise<Package | null> {
    const res = await fetch(`https://registry.npmjs.org/${name}`)
    if (!res.ok) return null
    return res.json()
  }
}

class CacheService {
  private cache = new Map<string, { value: unknown; expires: number }>()

  get<T>(key: string): T | null {
    const entry = this.cache.get(key)
    if (!entry || entry.expires < Date.now()) return null
    return entry.value as T
  }

  set(key: string, value: unknown, ttl: number): void {
    this.cache.set(key, { value, expires: Date.now() + ttl * 1000 })
  }
}

// Awilix injects by matching constructor parameter names to container registrations:
class PackageService {
  constructor(
    private packageRepository: PackageRepository,  // ← name matches registration
    private cacheService: CacheService             // ← name matches registration
  ) {}

  async getHealth(name: string) {
    const cached = this.cacheService.get<PackageHealth>(`health:${name}`)
    if (cached) return cached

    const pkg = await this.packageRepository.findByName(name)
    // ...
  }
}

// Container:
const container = createContainer()

container.register({
  packageRepository: asClass(PackageRepository, { lifetime: Lifetime.SINGLETON }),
  cacheService: asClass(CacheService, { lifetime: Lifetime.SINGLETON }),
  packageService: asClass(PackageService, { lifetime: Lifetime.SINGLETON }),
})

// Resolve:
const packageService = container.resolve<PackageService>("packageService")

Scoped lifetime (per-request)

import { createContainer, asClass, Lifetime } from "awilix"
import express from "express"

const container = createContainer()

container.register({
  packageRepository: asClass(PackageRepository, { lifetime: Lifetime.SINGLETON }),
  requestLogger: asClass(RequestLogger, { lifetime: Lifetime.SCOPED }),  // Per-request
  packageService: asClass(PackageService, { lifetime: Lifetime.SCOPED }),
})

const app = express()

// Create a scope per request:
app.use((req, res, next) => {
  req.container = container.createScope()  // New scope for each request
  next()
})

app.get("/packages/:name", async (req, res) => {
  // resolve from the request scope:
  const service = req.container.resolve<PackageService>("packageService")
  const health = await service.getHealth(req.params.name)
  res.json(health)
})

TSyringe

TSyringe — Microsoft's lightweight DI container:

Setup

// tsconfig.json (same as InversifyJS):
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

import "reflect-metadata"  // Import at app entry point

Usage

import { injectable, inject, container, singleton } from "tsyringe"

// Service identifier tokens:
const PackageRepositoryToken = "PackageRepository"

@injectable()
class NpmPackageRepository implements IPackageRepository {
  async findByName(name: string): Promise<Package | null> {
    const res = await fetch(`https://registry.npmjs.org/${name}`)
    return res.ok ? res.json() : null
  }
}

@singleton()  // Register as singleton automatically
class PackageService {
  constructor(
    @inject(PackageRepositoryToken) private repo: IPackageRepository
  ) {}

  async getHealth(name: string) {
    const pkg = await this.repo.findByName(name)
    return pkg ? calculateHealth(pkg) : null
  }
}

// Register:
container.register(PackageRepositoryToken, { useClass: NpmPackageRepository })

// Resolve:
const service = container.resolve(PackageService)

Testing with TSyringe

import { container } from "tsyringe"

beforeEach(() => {
  container.clearInstances()  // Clear between tests
})

test("gets package health", async () => {
  // Register mock:
  container.register(PackageRepositoryToken, {
    useValue: {
      findByName: vi.fn().mockResolvedValue({ name: "react", version: "18.3.1" }),
    },
  })

  const service = container.resolve(PackageService)
  const health = await service.getHealth("react")
  expect(health).toBeDefined()
})

Feature Comparison

FeatureInversifyJSAwilixTSyringe
Decorators
reflect-metadata✅ Required✅ Required
Auto-wire✅ By name
Scoped lifetime
TypeScript
Circular deps✅ (lazyInject)⚠️
Bundle size~100KB~30KB~15KB
ComplexityHighMediumLow
TestingGoodExcellentGood

When to Use Each

Choose InversifyJS if:

  • Large enterprise TypeScript applications with complex dependency graphs
  • You're familiar with Spring or NestJS-style DI patterns
  • You need full IoC features: middleware, conditional binding, factory injection
  • Team has experience with IoC containers

Choose Awilix if:

  • You want DI without decorator/reflect-metadata overhead
  • ESM or Bun environments where decorators are problematic
  • Functional style is preferred over OOP decorators
  • Scoped container per HTTP request (Awilix excels here)

Choose TSyringe if:

  • You want InversifyJS-style decorators with less boilerplate
  • Building a smaller application where full InversifyJS is overkill
  • Microsoft ecosystem — TSyringe pairs well with other MS tools

Consider NOT using a DI container if:

  • Small/medium application — manual dependency passing is simpler
  • Using NestJS — its built-in DI is already excellent
  • The overhead of decorators and reflect-metadata isn't worth it
  • Team isn't familiar with DI patterns — can add confusion without clear benefit

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on inversify v6.x, awilix v10.x, and tsyringe v4.x.

Compare developer tool and architecture packages on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.