
Security News
Axios Maintainer Confirms Social Engineering Attack Behind npm Compromise
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.
@novadi/core
Advanced tools
Annotation-free, Autofac-style dependency injection container for TypeScript
Annotation-free, blazing-fast dependency injection for TypeScript
NovaDI is a modern dependency injection container that keeps your business logic clean from framework code. No decorators, no annotations, no runtime reflection - just pure TypeScript and compile-time type safety.
Most TypeScript DI frameworks force you to pollute your code with decorators:
// ❌ Other frameworks - tight coupling everywhere
@Injectable()
class UserService {
constructor(
@Inject('ILogger') private logger: ILogger,
@Inject('IDatabase') private db: IDatabase
) {}
}
NovaDI keeps your code clean:
// ✅ NovaDI - clean, testable code
class UserService {
constructor(
private logger: ILogger,
private database: IDatabase
) {}
}
// DI configuration lives in ONE place (Composition Root)
const container = new Container()
const builder = container.builder()
builder.registerType(ConsoleLogger).as<ILogger>().singleInstance()
builder.registerType(PostgresDatabase).as<IDatabase>().singleInstance()
builder.registerType(UserService).as<UserService>()
const app = builder.build()
const userService = app.resolveType<UserService>()
Your business logic stays framework-agnostic. Your tests stay simple. Your architecture stays clean.
npm install @novadi/core
# or
yarn add @novadi/core
# or
pnpm add @novadi/core
NovaDI uses a TypeScript transformer to automatically inject type names at compile-time. This enables clean, annotation-free code while maintaining full type safety.
Why a transformer? TypeScript erases all type information at runtime. The transformer captures type names during compilation, enabling powerful features like dependency graph generation, compile-time validation, circular dependency detection, and automated wiring - all with zero runtime overhead.
Use unplugin for universal bundler support. This is the easiest and most reliable approach.
Vite:
// vite.config.ts
import { defineConfig } from 'vite'
import { NovadiUnplugin } from '@novadi/core/unplugin'
export default defineConfig({
plugins: [NovadiUnplugin.vite()]
})
webpack:
// webpack.config.js
const { NovadiUnplugin } = require('@novadi/core/unplugin')
module.exports = {
plugins: [NovadiUnplugin.webpack()]
}
Rollup:
// rollup.config.js
import { NovadiUnplugin } from '@novadi/core/unplugin'
export default {
plugins: [NovadiUnplugin.rollup()]
}
esbuild:
// esbuild.config.js
const { NovadiUnplugin } = require('@novadi/core/unplugin')
require('esbuild').build({
plugins: [NovadiUnplugin.esbuild()]
})
For direct tsc compilation, use ts-patch:
npm install -D ts-patch
npx ts-patch install
Add to tsconfig.json:
{
"compilerOptions": {
"plugins": [
{ "transform": "@novadi/core/transformer" }
]
}
}
That's it! The transformer handles everything automatically:
.as<ILogger>() → transformer injects "ILogger".as<UserService>() → transformer injects "UserService".autoWire() → transformer generates mapResolvers arrayNo manual type names needed!
import { Container } from '@novadi/core'
// 1. Define your services (clean code, no decorators!)
interface ILogger {
log(message: string): void
}
class ConsoleLogger implements ILogger {
log(message: string) {
console.log(`[LOG] ${message}`)
}
}
class UserService {
constructor(private logger: ILogger) {}
createUser(name: string) {
this.logger.log(`Creating user: ${name}`)
}
}
// 2. Configure container (Composition Root)
const container = new Container()
const builder = container.builder()
// Register implementations
builder.registerType(ConsoleLogger).as<ILogger>().singleInstance()
// Transformer automatically wires ALL dependencies!
builder.registerType(UserService).as<UserService>()
const app = builder.build()
// 3. Resolve and use
const userService = app.resolveType<UserService>()
userService.createUser('Alice') // [LOG] Creating user: Alice
That's it! No manual configuration. No mapping. The transformer does it all automatically.
The logger parameter automatically resolves to the registered ILogger interface by naming convention. This is THE way to use NovaDI.
Autowiring by convention is THE way you wire dependencies. No manual configuration, no boilerplate - it just works.
class UserService {
constructor(
private logger: ILogger, // Automatically resolves ILogger by convention
private database: IDatabase // Automatically resolves IDatabase by convention
) {}
}
// This is all you need - transformer does the rest!
builder.registerType(UserService).as<UserService>()
How it works:
mapResolvers array automatically.autoWire({ mapResolvers: [...] }) into the callThis is how you should wire ALL your services. Transformer-powered autowiring - always.
Why you need explicit mapping: Transformeren kan kun autowire typed dependencies (interfaces/classes). For primitives, strings, og configuration values skal du bruge explicit mapping.
Common use case - API Client:
interface IHttpClient {
get<T>(endpoint: string): Promise<T>
post<T>(endpoint: string, data: any): Promise<T>
}
class ApiClient implements IHttpClient {
constructor(
private baseUrl: string, // ⚠️ Primitive - transformer can't autowire this
private apiKey: string, // ⚠️ Primitive - transformer can't autowire this
private logger: ILogger // ✅ Typed dependency - transformer handles this
) {}
async get<T>(endpoint: string): Promise<T> {
this.logger.log(`GET ${this.baseUrl}${endpoint}`)
// HTTP logic...
}
}
// Explicit mapping for primitives + transformer for typed dependencies
builder.registerType(ApiClient).as<IHttpClient>().autoWire({
map: {
baseUrl: () => import.meta.env.VITE_API_BASE_URL, // Environment variable
apiKey: () => import.meta.env.VITE_API_KEY, // Secret from env
logger: (c) => c.resolveType<ILogger>() // Typed dependency
}
})
Benefits:
For regular service dependencies (no primitives), just register the type - transformer handles everything!
NEW: The transformer automatically detects array parameters and injects ALL registered implementations!
Common use case - Plugin System:
interface IPlugin {
name: string
execute(): void
}
class ValidationPlugin implements IPlugin {
name = 'validation'
execute() { /* validation logic */ }
}
class AuthPlugin implements IPlugin {
name = 'auth'
execute() { /* auth logic */ }
}
class PluginHost {
constructor(public plugins: IPlugin[]) {} // ✨ Array parameter
executeAll() {
this.plugins.forEach(p => p.execute())
}
}
// Register multiple implementations
builder.registerType(ValidationPlugin).as<IPlugin>()
builder.registerType(AuthPlugin).as<IPlugin>()
// Just works! Transformer auto-generates resolveTypeAll()
builder.registerType(PluginHost).as<PluginHost>()
const app = builder.build()
const host = app.resolveType<PluginHost>()
host.plugins.length // → 2 (both plugins injected automatically!)
Supported array syntaxes:
IFoo[] - Standard array syntax ✅Array<IFoo> - Generic array syntax ✅readonly IFoo[] - Readonly arrays ✅How it works:
(c) => c.resolveTypeAll("IPlugin") resolver[] returned if no implementations registeredPerfect for:
Important: Default lifetime is singleton (one instance for container lifetime).
builder.registerType(Database).as<IDatabase>()
// No explicit lifetime = singleton by default
Use for: Loggers, database connections, configuration, caches, most services
builder.registerType(RequestHandler).as<IRequestHandler>().instancePerDependency()
Use for: Request handlers, commands, stateful operations
builder.registerType(UnitOfWork).as<IUnitOfWork>().instancePerRequest()
Use for: Database transactions, request-scoped state
Register a type to be resolved by its concrete class name:
// Register as concrete type
builder.registerType(UserService).asSelf()
// Resolve by concrete type
const service = app.resolveType<UserService>()
Combine with interface registration (like Autofac's .AsSelf().As<ILogger>()):
// Register as both concrete type AND interface
builder.registerType(ConsoleLogger).asSelf().as<ILogger>()
// Can resolve both ways
const concrete = app.resolveType<ConsoleLogger>() // Works
const interface = app.resolveType<ILogger>() // Also works
import { Container } from '@novadi/core'
// Services (clean code, no framework imports!)
interface ILogger {
info(message: string): void
error(message: string, error?: Error): void
}
class ConsoleLogger implements ILogger {
info(message: string) { console.log(`[INFO] ${message}`) }
error(message: string, error?: Error) { console.error(`[ERROR] ${message}`, error) }
}
interface IDatabase {
query<T>(sql: string): Promise<T[]>
}
class PostgresDatabase implements IDatabase {
constructor(private logger: ILogger) {}
async query<T>(sql: string): Promise<T[]> {
this.logger.info(`Executing query: ${sql}`)
// Implementation...
return []
}
}
class UserService {
constructor(
private database: IDatabase,
private logger: ILogger
) {}
async getUser(id: number) {
this.logger.info(`Fetching user ${id}`)
return this.database.query(`SELECT * FROM users WHERE id = ${id}`)
}
}
// Composition Root
const container = new Container()
const builder = container.builder()
builder.registerType(ConsoleLogger).as<ILogger>().singleInstance()
builder.registerType(PostgresDatabase).as<IDatabase>().singleInstance()
builder.registerType(UserService).as<UserService>()
const app = builder.build()
// Use it
const userService = app.resolveType<UserService>()
await userService.getUser(123)
Notice:
new UserService(mockDB, mockLogger)Many DI frameworks (NestJS, InversifyJS, TypeDI, TSyringe) rely heavily on decorators. While convenient, this approach violates fundamental software design principles:
Your business logic should not know about the DI framework:
// ❌ BAD: Business logic tightly coupled to framework
import { Injectable, Inject } from 'some-di-framework'
@Injectable()
class OrderService {
constructor(
@Inject('PaymentGateway') private payment: IPaymentGateway,
@Inject('EmailService') private email: IEmailService,
@Inject('Logger') private logger: ILogger
) {}
processOrder(order: Order) {
// Business logic here...
}
}
Problems:
OrderService without the DI framework// ✅ GOOD: Clean business logic
class OrderService {
constructor(
private payment: IPaymentGateway,
private email: IEmailService,
private logger: ILogger
) {}
processOrder(order: Order) {
// Same business logic, zero framework coupling
}
}
// DI configuration lives separately (Composition Root)
const container = new Container()
const builder = container.builder()
// Transformer-powered autowiring - analyzes constructor types at compile-time
builder.registerType(OrderService).as<OrderService>()
Benefits:
OrderService can be instantiated without any frameworknew OrderService(mockPayment, mockEmail, mockLogger)// ❌ With decorators - need framework in tests
import { Test } from '@nestjs/testing'
describe('OrderService', () => {
it('processes order', async () => {
// Must set up entire DI framework for a simple test
const module = await Test.createTestingModule({
providers: [
OrderService,
{ provide: 'PaymentGateway', useValue: mockPayment },
{ provide: 'EmailService', useValue: mockEmail },
{ provide: 'Logger', useValue: mockLogger }
]
}).compile()
const service = module.get<OrderService>(OrderService)
// Finally can test...
})
})
// ✅ Without decorators - pure unit tests
describe('OrderService', () => {
it('processes order', () => {
const service = new OrderService(mockPayment, mockEmail, mockLogger)
// Test immediately, no framework needed
})
})
The Dependency Inversion Principle states: "High-level modules should not depend on low-level modules. Both should depend on abstractions."
When you add @Injectable() to a class, you're making it depend on the DI framework (a low-level module).
// ❌ Depends on DI framework (violation)
import { Injectable } from 'framework' // <- Infrastructure dependency
@Injectable() // <- Framework coupling
class BusinessService { /* ... */ }
// ✅ Depends on nothing (correct)
class BusinessService { /* ... */ }
NovaDI follows the Composition Root pattern - all DI configuration happens in ONE place at the application's entry point:
// main.ts - The ONLY place that knows about DI
import { Container } from '@novadi/core'
// All wiring happens here
const container = new Container()
const builder = container.builder()
// Infrastructure layer - singletons
builder.registerType(ConsoleLogger).as<ILogger>().singleInstance()
builder.registerType(PostgresDatabase).as<IDatabase>().singleInstance()
builder.registerType(StripePayment).as<IPaymentGateway>().singleInstance()
builder.registerType(SendGridEmail).as<IEmailService>().singleInstance()
// Service layer - autowired by transformer
builder.registerType(OrderService).as<OrderService>()
builder.registerType(UserService).as<UserService>()
// Application layer
builder.registerType(Application).as<Application>()
const app = builder.build()
// Start application
const application = app.resolveType<Application>()
application.start()
Everything else is clean business code with zero DI knowledge.
NestJS/InversifyJS Style (Decorators Everywhere):
// user.service.ts
import { Injectable, Inject } from '@nestjs/common'
@Injectable()
export class UserService {
constructor(
@Inject('ILogger') private logger: ILogger,
@Inject('IDatabase') private db: IDatabase
) {}
}
// order.service.ts
import { Injectable, Inject } from '@nestjs/common'
@Injectable()
export class OrderService {
constructor(
@Inject('IPayment') private payment: IPayment,
@Inject(UserService) private users: UserService
) {}
}
// Every file imports framework code!
// Every class is coupled to the DI container!
// Cannot test without framework!
NovaDI Style (Clean Separation):
// user.service.ts
export class UserService {
constructor(
private logger: ILogger,
private database: IDatabase
) {}
}
// order.service.ts
export class OrderService {
constructor(
private payment: IPayment,
private users: UserService
) {}
}
// main.ts (Composition Root)
const container = new Container()
const builder = container.builder()
builder.registerType(UserService).as<UserService>()
builder.registerType(OrderService).as<OrderService>()
const app = builder.build()
// Business code knows nothing about DI!
// Tests are trivial: new UserService(mockLogger, mockDb)
// Framework can be swapped without touching services!
builder
.register((c) => {
const config = c.resolveType<IConfig>()
const logger = c.resolveType<ILogger>()
return new ComplexService(config, logger, new Date())
})
.as<IComplexService>()
.singleInstance()
const config = { apiKey: 'secret', timeout: 5000 }
builder.registerInstance(config).as<IConfig>()
// Create child scope per request
app.use((req, res, next) => {
const requestScope = app.createChild()
req.container = requestScope
next()
})
// Resolve per-request services
const handler = req.container.resolveType<IRequestHandler>()
// Register multiple implementations of same interface
interface IStorageProvider {
get(key: string): any
set(key: string, value: any): void
}
class LocalStorageProvider implements IStorageProvider { /* ... */ }
class SessionStorageProvider implements IStorageProvider { /* ... */ }
// Register with keys
builder.registerType(LocalStorageProvider).as<IStorageProvider>().keyed('local')
builder.registerType(SessionStorageProvider).as<IStorageProvider>().keyed('session')
// Resolve specific implementation by key
const localStorage = app.resolveKeyed<IStorageProvider>('local')
const sessionStorage = app.resolveKeyed<IStorageProvider>('session')
For the curious developers who want to know how it works under the hood.
NovaDI uses a three-tier resolution strategy for maximum speed:
private readonly ultraFastSingletonCache: Map<Token<any>, any> = new Map()
resolve<T>(token: Token<T>): T {
// Zero overhead - direct Map lookup
const ultraFast = this.ultraFastSingletonCache.get(token)
if (ultraFast !== undefined) {
return ultraFast // ⚡ Instant return, no checks
}
// ...
}
Performance: O(1) - Hash map lookup, ~1-2 CPU cycles Use case: Singleton services (most common in real apps)
private readonly fastTransientCache: Map<Token<any>, Factory<any>> = new Map()
// Skip ResolutionContext entirely for simple cases
const fastTransient = this.fastTransientCache.get(token)
if (fastTransient) {
return fastTransient(this) // No context overhead
}
Performance: O(1) - Direct factory call, no context allocation Use case: Transient services with no dependencies
// Full resolution with circular dependency detection
const context = this.currentContext || Container.contextPool.acquire()
context.enterResolve(token)
try {
return this.resolveWithContext(token, context)
} finally {
context.exitResolve(token)
}
Performance: O(n) where n = dependency chain depth Use case: Per-request scoped or complex dependency graphs
To reduce garbage collection pressure, NovaDI pools ResolutionContext objects:
class ResolutionContextPool {
private pool: ResolutionContext[] = []
private readonly maxSize = 10
acquire(): ResolutionContext {
return this.pool.pop() ?? new ResolutionContext()
}
release(context: ResolutionContext): void {
if (this.pool.length < this.maxSize) {
context.reset() // Clear state
this.pool.push(context)
}
}
}
Benefit: Reduces heap allocations by ~90% for typical usage patterns
Dependency resolution paths are only built when errors occur:
class ResolutionContext {
private path?: string[] // Lazy initialization
getPath(): string[] {
if (!this.path) {
// Only build when needed (error reporting)
this.path = Array.from(this.resolvingStack).map(t => t.toString())
}
return this.path
}
}
Benefit: Avoids expensive toString() calls during successful resolutions
Container instance: ~4 KB
+ Bindings: ~100 bytes per service
+ Singleton cache: ~50 bytes per singleton
+ Context pool: ~2 KB (10 pooled contexts)
For a typical app with 50 services:
Run on Node.js 20, M1 MacBook Pro
| Operation | Time | Ops/sec |
|---|---|---|
| Resolve singleton (ultra-fast) | ~10 ns | 100M |
| Resolve transient (fast) | ~50 ns | 20M |
| Resolve with dependencies | ~200 ns | 5M |
| Container build (50 services) | ~2 ms | - |
Comparison:
| Metric | Value |
|---|---|
| Total Lines of Code | 2,079 lines |
| Bundle Size (compiled) | ~59 KB |
| Public API Surface | 22 exports |
| Avg. Cyclomatic Complexity | ~3.4 (low, maintainable) |
| Runtime Dependencies | 0 (only TypeScript) |
File Breakdown:
container.ts - 706 lines (resolution engine)builder.ts - 498 lines (fluent API)transformer/index.ts - 544 lines (compile-time magic)autowire.ts - 229 lines (autowiring strategies)token.ts - 61 lines (type-safe tokens)errors.ts - 25 lines (error types)| Feature | NovaDI | InversifyJS | TSyringe | TypeDI | Brandi | Awilix |
|---|---|---|---|---|---|---|
| No Decorators | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ |
| AutoWire | ✅ Automatic | ❌ Manual | ❌ Manual | ❌ Manual | ❌ Manual | ✅ Automatic |
| Type Safety | ✅ Full | ⚠️ Partial | ⚠️ Partial | ⚠️ Partial | ✅ Full | ✅ Full |
| Transformer | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Performance | ⚡ ~10ns | 🐢 ~500ns | 🐢 ~300ns | 🐢 ~400ns | ⚡ ~10ns | ⚡ ~50ns |
| Bundle Size (gzipped) | 🥈 3.93 KB | 16.78 KB | 7.40 KB | 6.41 KB | 🥇 2.19 KB | ~4 KB |
| Composition Root | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ |
🥇 Brandi 2.19 KB (minified: 6.31 KB)
🥈 NovaDI 3.93 KB (minified: 13.99 KB) +79%
🥉 TypeDI 6.41 KB (minified: 21.96 KB) +192%
TSyringe 7.40 KB (minified: 25.35 KB) +238%
Inversify 16.78 KB (minified: 65.75 KB) +665%
NovaDI: 3.93 KB (gzipped) - Second smallest DI framework, only 79% larger than Brandi while offering full transformer-powered autowiring that Brandi lacks.
| Framework | Decorator-Free | Singleton | Transient | Build Time | Complex Graph | Bundle Size |
|---|---|---|---|---|---|---|
| NovaDI 🏆 | ✅ Yes | 0.03 ms | 0.08 ms | 0.10 ms | 0.04 ms 🥇 | 3.9 KB |
| Brandi | ✅ Yes | 0.11 ms | 0.20 ms | 0.11 ms | 0.08 ms | 2.2 KB 🥇 |
| TypeDI | ❌ No | 0.02 ms 🥇 | 0.03 ms 🥇 | 0.69 ms | 0.16 ms | 6.4 KB |
| TSyringe | ❌ No | 0.17 ms | 0.27 ms | 0.05 ms 🥇 | 9.74 ms | 7.4 KB |
| InversifyJS | ❌ No | 0.13 ms | 0.31 ms | 0.22 ms | 0.19 ms | 16.8 KB |
NovaDI wins on complex dependency graphs - The only metric that truly matters for real-world applications with deep dependency trees.
Copy this prompt when asking an AI assistant to help you use NovaDI:
I want to use the @novadi/core dependency injection library in my TypeScript project.
Key Principles:
- Package: @novadi/core
- NO decorators/annotations in business code
- Convention over configuration
- Uses .as<T>() and .resolveType<T>()
- TypeScript transformer handles type names automatically
Core API:
1. Import: import { Container } from '@novadi/core'
2. Build container:
const container = new Container()
const builder = container.builder()
3. Register services:
builder.registerType(ConsoleLogger).as<ILogger>().singleInstance()
4. Register services with dependencies:
builder.registerType(UserService).as<UserService>()
// Transformer analyzes constructor types and auto-generates .autoWire() with mapResolvers array
5. Build and resolve:
const app = builder.build()
const service = app.resolveType<UserService>()
Lifetimes:
- Default (no method call) - singleton (DEFAULT)
- .instancePerDependency() - transient
- .instancePerRequest() - per resolution tree
AutoWire (Transformer-Powered):
- Transformer automatically injects .autoWire() with mapResolvers from constructor types
- Just register the type: builder.registerType(UserService).as<UserService>()
- Use explicit map only for primitives/config values (transformer doesn't handle primitives)
Transformer Setup (tsconfig.json):
{
"compilerOptions": {
"plugins": [
{ "transform": "@novadi/core/transformer" }
]
}
}
Then: npm install -D ts-patch && npx ts-patch install
Simple Hello World example:
```typescript
import { Container } from '@novadi/core'
interface IGreeter {
greet(name: string): string
}
class ConsoleGreeter implements IGreeter {
greet(name: string): string {
return `Hello, ${name}!`
}
}
class Application {
constructor(private greeter: IGreeter) {}
run() {
console.log(this.greeter.greet('World'))
}
}
// Composition Root
const container = new Container()
const builder = container.builder()
builder.registerType(ConsoleGreeter).as<IGreeter>().singleInstance()
builder.registerType(Application).as<Application>().autoWire() // Convention!
const app = builder.build()
const application = app.resolveType<Application>()
application.run() // Outputs: Hello, World!
Please help me set up NovaDI following these patterns.
**For AI to reference:** Full README at https://github.com/janus007/NovaDI
---
## Documentation
- [Getting Started Guide](../../docs/getting-started.md)
- [Transformer Setup](../../docs/transformer-setup.md)
- [Best Practices](../../docs/best-practices.md)
- [Roadmap & Ideas](../../docs/roadmap.md)
---
## License
MIT © Janus Knudsen
---
## Acknowledgments
Inspired by:
- **Autofac** (.NET) - Composition Root pattern, fluent API
- **Awilix** (Node.js) - Clean, annotation-free API
- **Mark Seemann's** work on Dependency Injection patterns
Built for developers who believe in:
- Clean Architecture
- Separation of Concerns
- Testable Code
- SOLID Principles
- Convention over configuration
---
**Keep your code clean. Keep your architecture pure. Use NovaDI.**
FAQs
Annotation-free, Autofac-style dependency injection container for TypeScript
We found that @novadi/core demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.

Security News
The Axios compromise shows how time-dependent dependency resolution makes exposure harder to detect and contain.