This technique is not as popular as it should be.
What Is Node.js Dependency Injection?
Dependency injection is a code wiring technique that presumes that the dependencies are input by an external entity (passed as parameters or references), not hardcoded (created or required) inside a module. Dependencies are the objects or services that a module can use.
Dependency injections are a great benefit of JavaScript and many other languages because this technique helps create independent, scalable, and reusable components. At the same time, dependency injections in Node.js web development are not as popular as you might expect.
Most would say that dependency injection in NodeJS is unnecessary because you can import dependencies to a module using the command “require”. Yet, such an approach creates a more complex code to maintain and test.
NodeJS dependency injection solves this problem.
Key Reasons to Use Node.js Dependency Injection
In the realm of Node.js development, the strategic implementation aimed at the concept of dependency injection has become a pivotal approach for creating robust apps. By externalizing the management of individual components and their dependencies, developers can unlock a myriad of advantages. These advantages can significantly impact the efficiency and reliability of their codebase.
In this context, we delve into the various facets of why Node.js dependency injection is imperative. Our goal is to explore its role in fostering testable code, promoting code reusability, streamlining dependency management, and contributing to the overall efficiency and adaptability of modern apps. Let’s uncover the key advantages of dependency injection that make it a cornerstone in the development paradigm. After reading this section, you’ll have a comprehensive understanding of its benefits and implications for Node.js developers.
- Testable Code: robust dependency injection in Node.js enhances the testability of code. How? It allows developers to substitute real dependencies with mock objects during testing. This isolation helps pinpoint and resolve issues in specific modules without affecting the entire app. Hence, you get the opportunity to enjoy a more reliable and robust testing experience.
- Code Reusability: Techniques of dependency injection promote code reusability. They do this by decoupling components from their dependencies. This decoupling allows developers to easily swap out implementations without modifying the core functionality of the code. As a result, individual components become more adaptable and can be reused across different parts of an app or in entirely different projects with diverging libraries.
- Efficient Dependency Management: Dependency injection simplifies dependency management by centralizing the dependency resolution and configuration through a well-structured dependency injection container. This centralized approach streamlines the management of dependencies. It’s easier to handle changes and updates that are centralized rather than scattered throughout the entire codebase.
- Enhanced Maintainability: Centralized dependency management and decoupling of components contributes to enhanced maintainability. Changes to dependencies can be managed centrally, providing a clear overview of the dependencies used throughout the app. This centralized control reduces the risk of introducing errors during updates and makes the codebase more maintainable over time.
- Configuration Flexibility: Dependency injection allows for flexible configuration of dependencies. It enables developers to tailor the behavior of components without modifying their code. This flexibility is particularly advantageous in scenarios where different configurations are needed for various efficient application environments.
- Enhanced Code Readability: Dependency injection can lead to improved code readability by making the dependencies required by a component explicit. This transparency allows developers to understand the relationships between different components more easily. Consequently, the approach contributes to better collaboration and comprehension within a development team.
- Facilitates Modular Development: Dependency injection supports a modular development approach, where components are developed and tested independently before being integrated into the larger app. This modularity enhances code organization and simplifies the development and maintenance of complex applications. As a result, you get to use more efficient code than in the case of Property Injection.
- Promotes Scalability: With dependency injection, the scalability of Node.js apps is enhanced, as developers can add or modify components without disrupting the existing codebase. This scalability is crucial for apps that need to grow and evolve to meet changing requirements or accommodate increased loads from users.
Promoting dependency injection in Node.js
Node.js itself does not have built-in support for dependency injection (DI), but developers commonly use external libraries to implement DI in Node.js applications. Libraries like InversifyJS, Awilix, and Needle provide DI containers and facilitate dependency injection.
InversifyJS is a popular choice that offers a powerful, feature-rich DI container for Node.js, enabling developers to manage and inject dependencies seamlessly. Awilix is another library that supports DI with a focus on simplicity and performance. Needle is a lightweight DI container that integrates well with Node.js applications.
These libraries typically allow developers to define dependencies, manage their lifecycle, and inject them into classes or functions. Dependency injection in Node.js helps enhance code modularity, testability, and maintainability by reducing tight coupling between components. While not native to Node.js, these libraries provide effective solutions for implementing dependency injection in the ecosystem.
How to Perform Node.js Injection?
There are different ways of how dependency injection can be performed. You can find plenty of primitive examples on the web to understand the essence of this technique. Meanwhile, I would like to show you how to execute a dependency injection in an actual code with the help of the Frida framework.
Frida Framework
Frida by itself already has an example of code injection for Node.js, but it seems that it is a bit outdated and could work only with a particular Node.js version – GUIDE. It uses V8 embed code to execute a JS string. I have updated the code to Node.js v8.16.0 x64, but I will not elaborate on injection details here. You can easily find out more via the following link, so instead, let’s go to the injected code.At first, let’s see which Frida code we will use (you can get more details here).
- Module.findExportByName finds C exported function by its name. There is one little caveat – so-called “mangled names”. In short, when we investigate C++ compiled code, we could see a lot of names like ?GetCurrent@Isolate@v8@@SAPEAV12@XZ. First of our points is to find all required function names in a disassembler like IDA or OllyDbg and to get the memory address of it.
- NativeFunction(POINTER, RETURN_VALUE, ARGUMENTS) is used to define in-js bindings for a native function. Here, we need to describe correct arguments count and return values to call these functions later.
- NativeCallback is a code that will be executed after a native function call.
- WeakRef.bind is used to monitor specified pointer and the callback when it gets garbage-collected or on Frida detach.
- Memory.alloc(N) allocates N bytes in memory and returns pointer for this memory region.
Concepts and Functions Used
Here, I want to define some concepts that I am going to use:
- Isolate represents an isolated instance of the V8 engine. V8 isolates have completely separate states. Objects from one isolate cannot be used in another isolate. When V8 is initialized, a default isolate is implicitly created and entered. The embedder can create additional isolates and use them in parallel in multiple threads. An isolate can be entered by at most one thread at any given point in time. The Locker/Unlocker API must be used to synchronize. In short, it is a kind of sandbox, which contains its own states.
- Context stands for a sandboxed execution context with its own set of built-in objects and functions.
- HandleScope is a stack-allocated class, which governs a number of local handles. After a handle scope has been created, all local handles will be allocated within that handle scope until either the handle scope is deleted or another handle scope is created. If there is already a handle scope and a new one is created, all allocations will take place in the new handle scope until it is deleted. After that, new handles will again be allocated in the original handle scope. After the handle scope of a local handle has been deleted, the garbage collector will no longer track the object stored in the handle and may deallocate it. The behavior of accessing a handle for which the handle scope has been deleted is undefined.
- Script stands for a compiled JavaScript script.
- String is a JS string value.
- Value is a superclass of all JavaScript values and objects.
Next, I describe the functions of libuv that we will use – a lib that allows using an async, event-driven style of programming:
- uv_async_init initializes handle, specifies the callback that will be executed inside the event loop;
- uv_default_loop takes default event loop;
- uv_async_send calls the callback;
- uv_unref destroys the object created;
- uv_close requests handle to be closed.
We will also use some V8 functions (formatted as real method name –> name of binded js function). In particular:
- V8::Isolate::GetCurrent(v8_Isolate_GetCurrent) takes the instance of a current Isolate.
- V8::Isolate::GetCurrentContext(v8_Isolate_GetCurrentContext) gets context from the current Isolate.
- V8::Context::Enter(v8_Context_Enter) enters this context. After entering a context, all code compiled and run is compiled and run in this context. If another context is already entered, this old context is saved so it can be restored when the new context is exited.
- V8::HandleScope::Init(v8_HandleScope_init) initializes HandleScope.
- V8::String::NewFromUtf8(v8_String_NewFromUtf8) gets JS string from const char *.
- V8::Script::Compile(v8_Script_Compile) compiles the specified script (bound to current context).
- V8::Script::Run(v8_Script_Run) runs the script returning the resulting value. If the script is context-independent (created using ::New), it will be run in the currently entered context. If it is context-specific (created using ::Compile), it will be run in the context in which it was compiled.
- v8::Value::Int32Value(v8_Value_Int32Value) gets C int from JS value. Value has a lot of functions for different types.
Code Listing
Let’s go through the code listing step by step:
createFunc is a helper that is used to create JS bind with a specified signature to reuse later.
All of the calls like const uv_default_loop = createFunc(‘uv_default_loop’, ‘pointer’, []) are required to define JS bindings to native functions.
scriptToExecute is our injected code.
We define uv async handler, then bind processPending to the default event loop, then “wake up” that event loop for a callback call. Inside the callback, we receive an Isolate instance, initialize new HandleScope, and take current context. Then, we convert the JS string to V8 string, compile, and run it. After all, we just extract C int value from the script execution result.
To Wrap Up
Node js dependency injection is not as popular as it should be. It is designed to simplify and create a time-efficient coding experience, not vice versa.
There are different ways to perform this technique, and the one I offered you here is just one of them. You can find plenty of other examples on StackOverflow.
Yet, regardless of how you implement it, the benefits remain simple maintenance, testing, management, and single responsibility.
Does it seem to you that Node.js is the framework you need? Please, find out more about Node.js development services that we offer.
Written by Anton Trofimov, Full-stack JavaScript Developer, and Tania Matviiok, Content Manager for helping with the article @ Keenethics