InversifyJS vs Awilix vs TSyringe: Dependency Injection in Node.js (2026)
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: trueandemitDecoratorMetadata: 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
Download Trends
| Package | Weekly Downloads | Decorators | reflect-metadata | Auto-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
| Feature | InversifyJS | Awilix | TSyringe |
|---|---|---|---|
| Decorators | ✅ | ❌ | ✅ |
| reflect-metadata | ✅ Required | ❌ | ✅ Required |
| Auto-wire | ❌ | ✅ By name | ❌ |
| Scoped lifetime | ✅ | ✅ | ✅ |
| TypeScript | ✅ | ✅ | ✅ |
| Circular deps | ✅ (lazyInject) | ✅ | ⚠️ |
| Bundle size | ~100KB | ~30KB | ~15KB |
| Complexity | High | Medium | Low |
| Testing | Good | Excellent | Good |
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 →