Dependency Injection Container for JavaScript (Node.js) — Awilix
In modern Node.js development, code architecture cleanliness and testability come to the forefront. One of the key tools for achieving these goals is Dependency Injection (DI). The Awilix library provides a powerful, performant, and flexible DI container for JavaScript and TypeScript, ideally suited for server-side Node.js applications. In this article, we will thoroughly explore how to use Awilix for managing dependencies, improving modularity, and simplifying testing.
What is Dependency Injection and why do you need a container?
Dependency Injection is a design pattern where an object receives its dependencies from the outside, rather than creating them itself. This implements the Inversion of Control (IoC) principle. A container is a tool that automates the process of creating and providing these dependencies. Instead of manually creating class instances and passing them to each other, you describe the assembly rules in the container, and it resolves all dependency chains itself.
Main advantages of using a DI container:
- Reduced code coupling: Modules depend on abstractions, not concrete implementations.
- Simplified testing: Easy to replace real services with mocks during unit tests.
- Centralized management: All dependency configuration is in one place, simplifying refactoring.
- Lifecycle management: The container can control how long an object lives — one instance for the entire application (singleton) or a new one for each request.
Installing Awilix
To get started, install the library via npm:
npm install awilixAwilix has no external dependencies and works with both CommonJS and ESM modules.
Core Concepts of Awilix
Awilix is built around several key concepts:
- Container: The main object that stores and provides dependencies.
- Registration: The process of binding a name (token) to a factory, class, or value.
- Resolver: An object that determines how exactly to create or retrieve a dependency. For example,
asClass(),asFunction(),asValue(). - Lifetime: Specifies how many times an object will be created:
ScopedLifeTime(once per scope),SingletonLifeTime(once for the entire lifetime), orTransientLifeTime(new every time).
Usage Example: User Management Service
Let's look at a simple example. Suppose we have a database, a user repository, and a service for business logic.
1. Define the classes
// database.jsclass Database { constructor() { this.connection = 'Connected to DB'; } query(sql) { return `Result for: ${sql}`; }}
// userRepository.jsclass UserRepository { constructor({ database }) { this.db = database; } findById(id) { return this.db.query(`SELECT * FROM users WHERE id = ${id}`); }}
// userService.jsclass UserService { constructor({ userRepository }) { this.repo = userRepository; } getUser(id) { return this.repo.findById(id); }}Notice the constructors: they accept an object with dependencies. This is standard practice for Awilix.
2. Configure the container
const { createContainer, asClass, asValue, Lifetime } = require('awilix');
const container = createContainer();
// Register the Also in library
Working with CSV in PHP: Reading, Writing, and Data Processing
Processing and Parsing Markdown in PHP: Libraries and Examples
Task Queues in Kotlin: Coroutines, Flow and Libraries for Background Processing
Elasticsearch client for Kotlin: integration, examples, and capabilities
Parsing HTML in PHP: Overview of Libraries and Tools