NovaDI

Decorator-free Dependency Injection for TypeScript

Inspired by Autofac, built for modern TypeScript projects. Fast, type-safe, and framework-agnostic.

TypeScript 5+ Zero Decorators Browser-First Type-Safe 3.93KB gzipped

True Separation of Concerns

Zero decorators in your domain code. No @Injectable() pollution. All DI configuration in ONE place at the composition root - just like Autofac in .NET.

Industry-Leading Speed

Fastest singleton resolution (0.03ms) and fastest complex graph (0.03ms) among all tested frameworks. No reflection overhead, just pure TypeScript performance.

Modern & Universal

Only 3.93KB gzipped. Works everywhere: Browser, Node.js, Deno, Edge Workers. Built for TypeScript 5+ with full type safety and zero runtime dependencies. Tested with 5 major bundlers (Vite, Webpack, Rollup, esbuild, Parcel) using unplugin for seamless integration.

Quick Start (30 seconds)

Get up and running with NovaDI in less than a minute

1. Install

npm install @novadi/core

2. Use

import { Container } from '@novadi/core'

const container = new Container()
  .builder()
  .registerType(ConsoleLogger)
    .as<ILogger>()
    .singleInstance()
  .build()

const logger = container.resolveType<ILogger>()
logger.log('Hello NovaDI!')

Why NovaDI?

Most DI frameworks require decorators everywhere in your domain code. NovaDI takes a different approach.

❌ Other Frameworks

// Decorators pollute your domain code
@Injectable()
class UserService {
  constructor(
    @Inject('Logger') private logger: ILogger,
    @Inject('Database') private db: IDatabase
  ) {}
}
  • Tight coupling to framework
  • Violates separation of concerns
  • Hard to test in isolation
  • Reflection overhead

✅ NovaDI Approach

// Clean domain code
class UserService {
  constructor(
    private logger: ILogger,
    private db: IDatabase
  ) {}
}

// DI config in ONE place (composition root)
const container = new Container()
  .builder()
  .registerType(Logger).as<ILogger>()
  .registerType(Database).as<IDatabase>()
  .registerType(UserService).autoWire()
  .build()
  • Framework-agnostic domain code
  • True separation of concerns
  • Easy to test
  • Zero runtime reflection
💡 The Composition Root Pattern

NovaDI follows the Composition Root pattern: all DI configuration happens in ONE place at your application's entry point. Your domain code stays clean and framework-agnostic.

Key Features

Everything you need for modern dependency injection

Zero Decorators

Keep your domain code clean and framework-agnostic. No @Injectable(), no @Inject().

Industry-Leading Performance

Fastest singleton resolution (0.03ms) and complex graphs (0.03ms). Wins on speed with decorator-free design.

🎯

Type-Safe

Full TypeScript support without reflect-metadata. Leverages the type system for safety.

🏗️

Autofac-Inspired Builder

Fluent, readable API inspired by the excellent Autofac (.NET) container.

🔧

Autowiring

Automatic dependency resolution with paramName, map, or class-based strategies.

📦

Lightweight

3.93KB gzipped. Zero runtime dependencies. Tree-shakeable for optimal bundle size.

🌐

Universal

Works everywhere: Node.js, Browser, Edge Workers, Deno. No Node-specific APIs.

🔑

Keyed Services

Register multiple implementations of the same interface with keys.

👶

Child Containers

Scoped isolation for per-request patterns and hierarchical DI.

Performance

Benchmarked against popular DI frameworks

Framework Decorator-Free Singleton (ms) ↓ Transient (ms) ↓ Build Time (ms) ↓ Complex Graph (ms) ↓ Bundle Size (KB) ↓
NovaDI 🏆 ✅ Yes 0.03 🥇 0.09 0.11 0.03 🥇 3.9
Brandi ✅ Yes 0.11 0.22 0.12 0.07 2.2 🥇
InversifyJS ❌ No 0.14 0.31 0.29 0.12 16.8
TSyringe ❌ No 0.19 0.28 0.02 🥇 8.45 7.4
TypeDI ❌ No 0.03 🥇 0.03 🥇 3.41 0.21 6.4
📊 Benchmark Details

Benchmarks run on AMD Ryzen 7 PRO 6850H with 32GB RAM, measuring average resolution time over 100 runs.
Simple transients: classes with no dependencies. Complex graph: deep dependency tree with 5+ levels.

💡 About "Complex Graph"

The "Complex Graph" benchmark isn't actually complex - it's just how real enterprise applications are structured. This isn't a contrived "hello world" demo meant to impress with artificial depth.

A typical application service (like UserService) needs a repository, which needs an HTTP client, which needs configuration and logging. That's 3-4 dependency levels - exactly what you'd build in any production app. The benchmark measures how fast the container resolves this realistic object graph, not some artificial "look how deep we can go" scenario.

Real-World Example

Complete application setup with multiple services

1. Define Interfaces (Domain Layer)

// interfaces.ts - Clean, no DI knowledge
interface ILogger {
  log(message: string): void
  error(message: string): void
}

interface IDatabase {
  query(sql: string): Promise<any>
}

interface IUserService {
  getUser(id: string): Promise<User>
  createUser(data: CreateUserDto): Promise<User>
}

2. Implement Services

// implementations.ts
class ConsoleLogger implements ILogger {
  log(message: string) {
    console.log(`[LOG] ${message}`)
  }
  error(message: string) {
    console.error(`[ERROR] ${message}`)
  }
}

class PostgresDatabase implements IDatabase {
  async query(sql: string) {
    // ... database logic
  }
}

class UserService implements IUserService {
  constructor(
    private logger: ILogger,
    private db: IDatabase
  ) {}

  async getUser(id: string) {
    this.logger.log(`Fetching user ${id}`)
    return this.db.query(`SELECT * FROM users WHERE id = $1`, [id])
  }

  async createUser(data: CreateUserDto) {
    this.logger.log(`Creating user ${data.email}`)
    return this.db.query(`INSERT INTO users ...`, data)
  }
}

3. Configure DI (Infrastructure Layer)

// composition-root.ts - ALL DI config in ONE place
const container = new Container()
  .builder()

  // Register infrastructure services
  .registerType(ConsoleLogger)
    .as<ILogger>()
    .singleInstance()

  .registerType(PostgresDatabase)
    .as<IDatabase>()
    .singleInstance()

  // Register domain services with autowiring
  .registerType(UserService)
    .as<IUserService>()
    .autoWire({
      map: {
        logger: (c) => c.resolveType<ILogger>(),
        db: (c) => c.resolveType<IDatabase>()
      }
    })
    .singleInstance()

  .build()

4. Use in Application

// app.ts
const userService = container.resolveType<IUserService>()

// All dependencies automatically injected!
const user = await userService.getUser('123')
console.log(user)

Ready to Get Started?

Build better TypeScript applications with true dependency injection

Documentation

📘 Getting Started

Learn the basics: installation, first container, core concepts, and common patterns.

Read Documentation →

📗 Complete Documentation

All features from easy to advanced: autowiring, keyed services, child containers, performance.

Explore features →

📙 DI Theory & Philosophy

Why dependency injection matters and why decorators are an anti-pattern.

Learn theory →