logo

Velocity Stack

Dependency Injections In NodeJS.

Martin Kariuki 7 October 2024

nada

Dependency Injection (DI) in Node.js is a design pattern that helps manage the dependencies (or services) that different parts of your application need to function. It allows for a more modular, testable, and maintainable codebase. Let’s break it down in simple terms.

What is Dependency Injection?

  1. Dependency: A dependency is any external service or object that a class or function needs to perform its task. For example, a UserService might need a Database object to store and retrieve users.

  2. Injection: Injection refers to the process of providing these dependencies (like Database) to the class or function when they need them, rather than the class or function creating the dependency themselves.

In simpler words, instead of a class or function "building" or "finding" what it needs, it "asks for it" when it's created. The idea is to give components what they need without hard-coding those things inside.

Why Use Dependency Injection?

  1. Flexibility: You can easily swap dependencies. For example, if your UserService relies on a specific Database, but you want to switch to a different database, you can do so without modifying the UserService code.

  2. Testability: DI makes unit testing easier. Instead of using real dependencies (like a live database), you can inject mock objects during testing. This makes tests faster and more reliable.

  3. Decoupling: It reduces the direct dependencies between classes and services. Each part of your application knows only what it needs, without worrying about how those dependencies are created.

Dependency Injection in Node.js

In Node.js, you can implement dependency injection in various ways. Node.js doesn't have a built-in DI framework like Angular, but you can implement DI manually, or use libraries like inversify or typedi.

Here’s a simple example of how manual dependency injection works in Node.js.

Example

Let’s imagine you have a UserService that needs a Logger and a Database. Without DI, you might see this:

class UserService {
    private logger: Logger;
    private db: Database;

    constructor() {
        this.logger = new Logger();
        this.db = new Database();
    }

    createUser(name: string) {
        this.logger.log(`Creating user ${name}`);
        this.db.save(name);
    }
}

In the above example:

UserService is responsible for creating Logger and Database. This tightly couples UserService to the Logger and Database implementations, making it hard to change or test. Now, let’s refactor using Dependency Injection:

class UserService {
    private logger: Logger;
    private db: Database;

    // Instead of creating the dependencies, we "inject" them through the constructor
    constructor(logger: Logger, db: Database) {
        this.logger = logger;
        this.db = db;
    }

    createUser(name: string) {
        this.logger.log(`Creating user ${name}`);
        this.db.save(name);
    }
}

Now, the UserService only "knows" that it needs a Logger and Database, but it doesn't create them. These are provided (or injected) from the outside.

Usage Example

You can inject the dependencies when you create the UserService:

const logger = new Logger();
const db = new Database();
const userService = new UserService(logger, db);  // Dependencies injected here

userService.createUser('Jane Doe');

This makes it much easier to switch or mock dependencies, especially in tests:

// In testing, you can inject mock dependencies
const mockLogger = { log: (message: string) => console.log(`Mock log: ${message}`) };
const mockDb = { save: (name: string) => console.log(`Mock save: ${name}`) };

const userService = new UserService(mockLogger, mockDb);
userService.createUser('Test User');  // Uses the mock implementations

Use Cases for Dependency Injection

Building Modular Applications: When your app grows, it will have multiple services like UserService, EmailService, etc. DI helps manage and inject these services where needed without directly coupling them.

Testing: One of the biggest advantages of DI is for unit testing. It allows you to inject mock or fake dependencies instead of real services like databases, which can make your tests faster and more isolated.

Example: If you’re testing UserService, you don’t want to connect to a real database every time. Instead, you inject a mock database that mimics the behavior you need for the test.

Decoupling: You can change dependencies without modifying the core logic of your application. If you want to switch databases, logging mechanisms, or external APIs, you can inject new implementations without rewriting the logic in your services.

Inversion of Control (IoC): In a traditional app, classes often control the creation of their own dependencies. With DI, this is reversed — external code (like a DI container) controls how dependencies are created and provided.

DI Containers

A DI Container (like the one in the earlier example) helps manage all of these dependencies in one place. Instead of manually creating and injecting dependencies, you register them with the container, and it handles the creation and injection for you.

Example:

const container = new DependencyContainer();
container.register('logger', new Logger());
container.register('db', new Database());

const userService = container.resolve<UserService>('userService');

This pattern is helpful in larger applications to avoid manually creating and passing around dependencies.

Summary

  • Dependency Injection is a pattern that helps you inject external services into classes or functions rather than creating those services inside the class. It makes your code more modular, testable, and flexible. You can use DI manually in Node.js or with the help of a DI container/library to manage dependencies efficiently in larger applications.