惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

云风的 BLOG
云风的 BLOG
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 叶小钗
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
V2EX
酷 壳 – CoolShell
酷 壳 – CoolShell
月光博客
月光博客
人人都是产品经理
人人都是产品经理
宝玉的分享
宝玉的分享
博客园 - 司徒正美
WordPress大学
WordPress大学
Microsoft Azure Blog
Microsoft Azure Blog
罗磊的独立博客
Vercel News
Vercel News
T
The Blog of Author Tim Ferriss
T
Tailwind CSS Blog
A
About on SuperTechFans
Apple Machine Learning Research
Apple Machine Learning Research
L
LangChain Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
V
Visual Studio Blog
S
SegmentFault 最新的问题
Google DeepMind News
Google DeepMind News
博客园 - 聂微东

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
I've been doing Dependency Injection in Node.js without d...
Mauro Gadaleta · 2026-06-18 · via DEV Community

Ok so first, let me be honest. This post is partly me venting.

I've been maintaining node-dependency-injection for about 9 years now. 300 stars on GitHub. Not exactly viral. And every time I look at InversifyJS or tsyringe climbing in popularity I think "yeah, but at what cost".

So let me explain my problem with decorators.


The coupling nobody talks about

When you write this:

@Injectable()
export class UserService {
  constructor(@Inject(MAILER_TOKEN) private mailer: IMailer) {}
}

Where does that @Injectable() live? In your DI framework. Which means your UserService — which is domain logic, business rules, the thing that should outlive any framework decision — now has a direct dependency on your IoC container library.

Your domain knows about your infrastructure. That's the wrong direction.

I know, I know. "It's just a decorator, it doesn't do anything". But it's still an import. It's still coupling. And if you ever want to swap the container, or move that service somewhere else, or just test it without spinning up the whole container — you now have to think about it.

With NDI, your service is just a class:

export class UserService {
  constructor(private mailer: IMailer) {}
}

That's it. No imports from my library. No decorators. No metadata. The service doesn't know it's being injected. The wiring lives completely outside — in a YAML file or in a bootstrap file. Your domain stays clean.


"But Symfony does decorators and it's fine"

Symfony doesn't use decorators in services actually. The DI config is external — YAML, XML, PHP config files. Your service is just a PHP class. That's literally what inspired NDI from the beginning.


What NDI actually does

Quick example. You have two payment providers and you want to inject the right one based on context:

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

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

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

// CheckoutService.ts — pure class, zero framework imports
export class CheckoutService {
  constructor(private payment: IPaymentService) {}

  async process(order: Order) {
    return this.payment.charge(order.total)
  }
}

The strategy pattern, completely outside the class. You can swap stripe for paypal by changing one line of config. Your CheckoutService literally doesn't care.


Autowire without decorators

This is the thing I'm most proud of honestly. NDI can read your TypeScript constructor types and wire everything automatically — without a single decorator:

// Just normal TypeScript. No imports from NDI.
export default class OrderService {
  constructor(
    private readonly repo: OrderRepository,
    private readonly mailer: MailService
  ) {}
}

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

const orders = container.get(OrderService) // fully wired

It parses the TypeScript AST, finds the constructor params, resolves them by type. No reflect-metadata, no decorators, no nothing. Just types.


Conditional services

One feature I don't see in other containers — register services based on environment:

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

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

In prod you have Redis, the memory cache never gets instantiated. Locally you don't, it falls back. Zero code changes.


Compiler passes

Borrowed from Symfony again. You can transform the container at build time before it compiles:

class AddLoggingPass implements CompilerPass {
  async process(container: ContainerBuilder) {
    for (const { id, definition } of container.findTaggedServiceIds('loggable')) {
      // wrap every tagged service with a logging decorator
      definition.setDecorator('logger.decorator')
    }
  }
}

container.addCompilerPass(new AddLoggingPass())

Try doing that cleanly with @Injectable decorators.


Why 300 stars after 9 years

Honestly? Because the ecosystem trained everyone to associate "TypeScript DI" with decorators. NestJS made it the default. And NestJS is great for many things — if you're all-in on the framework, the DI just comes along.

But if you care about Clean Architecture, hexagonal, keeping domain logic free from infrastructure concerns — decorators in your services is exactly what you're trying to avoid.

NDI is for people who want the DI container to be infrastructure, not something that bleeds into your domain. The wiring should be boring config, not annotations on your business logic.

I've been using this in production for 9 years. Works fine. And my UserService still doesn't know what a container is.