Dependency Injection in JavaScript: A Complete Guide

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: Why Your Code Needs Dependency Injection



Every JavaScript developer sooner or later faces the problem of "tightly coupled" code. Imagine a UserService class that internally creates an instance of Database and Logger. What happens when you want to test UserService without a real database? Or replace Logger with a different implementation? In the best case — you rewrite the entire class, in the worst case — you break the whole application.



The solution to this problem is Dependency Injection (DI). This is one of the key design principles that underlies the concept of Inversion of Control (IoC). In this article, we will break down what DI is, how to implement it in pure JavaScript (Node.js) without using frameworks, and why it is critically important for building scalable and testable applications.



What is Dependency Injection in Simple Terms?



Imagine you are building a house. Instead of embedding sockets into the walls permanently, you install standard outlets. Today you plug an old lamp into them, tomorrow — a smart light. You don't change the walls — you change the dependencies.



In programming, Dependency Injection is a pattern where an object receives its dependencies (other objects, services, configurations) from the outside, rather than creating them itself. Instead of writing:



// ❌ Bad: tight couplingclass UserService {    constructor() {        this.db = new Database('mongodb://...');        this.logger = new ConsoleLogger();    }}


We write:



// ✅ Good: dependency injectionclass UserService {    constructor(database, logger) {        this.db = database;        this.logger = logger;    }}


Now UserService does not know how Database and Logger are created. Its only job is to use them. This is inversion of control: control over creating dependencies shifts from the class to the calling code.



Types of Dependency Injection



There are three main ways to inject dependencies:

  • Constructor Injection — the most common and preferred method. Dependencies are passed when creating the object.
  • Property Injection — dependencies are set after the object is created. Convenient for optional dependencies.
  • Method Injection — the dependency is passed to a specific method when it is needed only for one action.


Writing a Simple DI Container in Node.js



In large projects, managing dependencies manually becomes difficult. This is where a DI container comes to the rescue — an object that knows how to create and store all the dependencies of your application. Let's write a simple but functional container in pure JavaScript.



Our container will support two modes: singleton (one instance for the entire application) and transient (a new instance on each request).



// di-container.jsclass DIContainer {    constructor() {        this.registry = new Map();        this.instances = new Map();    }

// Register a dependency with a factory function register(name, factory, { singleton = true } = {}) { this.registry.set(name, { factory, singleton }); if (singleton) { this.instances.delete(name); // Reset cache on re-registration } }

// Get an instance of a dependency resolve(name) { const item = this.registry.get(name); if (!item) { throw new Error(`Dependency "${name}" not found`); }

Blogs

Book Recommendations