Software Development Magazine - Project Management, Programming, Software Testing |
Scrum Expert - Articles, tools, videos, news and other resources on Agile, Scrum and Kanban |
InversifyJS - IoC container for JavaScript, TypeScript & Node.js
Remo H. Jansen, @RemoHJansen,
@inversifyjs, https://github.com/inversify/InversifyJS
InversifyJS is an inversion of control library that works in both frontend and backend JavaScript applications. InversifyJS is framework-agnostic and can be integrated with many existing frameworks like React applications powered by MobX or Node.js applications powered by express.
Web Site: https://github.com/inversify/InversifyJS
Version tested: 3.3.0
System Requirements: Node.js version 7.8.0 and TypeScript version 2.2.2 or higher
License & Pricing: MIT
Support: Github Issue tracker, https://github.com/inversify/InversifyJS/issues
Introduction
InversifyJS is a lightweight inversion of control (IoC) container for TypeScript and JavaScript apps. InversifyJS uses annotations to identify and inject its dependencies.
The InversifyJS API had been influenced by Ninject and Angular and encourages the usage of the best OOP and IoC practices.
InversifyJS has been developed with 4 main goals:
- Allow JavaScript developers to write code that adheres to the SOLID principles.
- Facilitate and encourage the adherence to the best OOP and IoC practices.
- Add as little runtime overhead as possible.
- Provide a state of the art development experience.
Motivation and background
Now that ECMAScript 2015 version of JavaScript supports classes and that TypeScript brings static types to JavaScript application, the SOLID principles have become more relevant than ever before in the development of JavaScript applications.
InversifyJS was created as a result of the need for tools to enable TypeScript developers to implement application that adhere to the SOLID principles.
I couple of years ago I was working on some TypeScript applications and I felt that there was a need for an IoC container with great support for TypeScript. At the time there was some IoC containers available for JavaScript applications but none of them were able to provide a developer experience as rich as I was expecting so I decided to try to develop something that would suit my needs.
The first commit to the InversifyJS core library took place the 7th of Apr 2015 and the version 1.0.0 was released on npm 10 days later. The version 2.0.0 was released the 11th of Sep 2016, after a year of development. The most recent release (3.3.0 at the time in which this article was published) was published in March 2017. Since the first release the project has earned over 1000 stars on GitHub, over 30 contributors and almost 25K monthly downloads on npm.
Getting Started
In this tutorial we are going to showcase how InversifyJS works using Node.js. InversifyJS can be used with JavaScript and TypeScript but it is recommended to use TypeScript for the best developer experience.
To get started you will need Node.js. You can download the Node.js binary for your operating system from the official downloads page: https://nodejs.org/en/download/
Once you install Node.js, you will need to install TypeScript. TypeScript can be installed using the npm command which is the default Node.js package manager:
$ npm install -g typescript@2.2.2 |
If both Node.js and TypeScript has been installed, you should be able to check the installed versions using the following commands.
$ node -v $ tsc -v |
At the time in which this article was published, the latest version of Node.js and TypeScript released were 7.8.0 and 2.2.2 respectively.
At this point, you should be ready to create a new project. We need to create a new folder named “inversify-nodejs-demo” and create a package.json file inside it. We can achieve this by using the npm init command as follows:
$ mkdir inversify-nodejs-demo $ cd inversify-nodejs-demo $ npm init --yes |
The preceding commands should generate file named “package.json” under the “inversify-nodejs-demo”. We can then install the “inversify” and “reflect-metadata” packages using the Node.js package manager:
$ npm install --save inversify@3.3.0 $ npm install --save reflect-metadata@0.1.10 |
The “reflect-metadata” module is as polyfill for the Reflect metadata API which is required by InversifyJS.
We also need to create a file named “tsconfig.json”. This file contains the configuration for the TypeScript compiler. We can create a “tsconfig.json” file using the following command:
$ tsc -init |
You can then copy the following into the generated “tsconfig.json”:
{ "compilerOptions": { "lib": ["es6"], "module": "commonjs", "target": "es5", "experimentalDecorators": true, "emitDecoratorMetadata": true } } |
The preceding configuration file contains some compilations required by InversifyJS. At this point we are ready to write a small demo. Let’s create a new TypeScript file named “index.ts”:
$ touch index.ts |
Let’s copy the following TypeScript code into the “index.ts” file:
import "reflect-metadata"; import { interfaces, injectable, inject, Container } from "inversify";
// 1. Declare interfaces interface Warrior { fight(): string; sneak(): string; }
interface Weapon { hit(): string; }
interface ThrowableWeapon { throw(): string; }
// 2. Declare types let TYPES = { Warrior: Symbol("Warrior"), Weapon: Symbol("Weapon"), ThrowableWeapon: Symbol("ThrowableWeapon") };
// 3. Declare classes @injectable() class Katana implements Weapon { public hit() { return "cut!"; } }
@injectable() class Shuriken implements ThrowableWeapon { public throw() { return "hit!"; } }
@injectable() class Ninja implements Warrior {
private _katana: Weapon; private _shuriken: ThrowableWeapon;
public constructor( @inject(TYPES.Weapon) katana: Weapon, @inject(TYPES.ThrowableWeapon) shuriken: ThrowableWeapon ) { this._katana = katana; this._shuriken = shuriken; }
public fight() { return this._katana.hit(); }; public sneak() { return this._shuriken.throw(); };
}
// 4. Create instance of Container & declare type bindings var myContainer = new Container(); myContainer.bind<Warrior>(TYPES.Warrior).to(Ninja); myContainer.bind<Weapon>(TYPES.Weapon).to(Katana); myContainer.bind<ThrowableWeapon>(TYPES.ThrowableWeapon).to(Shuriken);
// 5. Resolve Warrior type var ninja = myContainer.get<Warrior>(TYPES.Warrior);
// 6. Check “Katana” and “Shuriken” has been injected into “Ninja” console.log(ninja.fight()); // "cut!" console.log(ninja.sneak()); // "hit!" |
The preceding file performs the following of tasks:
- Import the required dependencies “reflect-metadata” and “inversify”.
- Declare some interfaces and some types. Types are unique identifiers used to represent interfaces at runtime. We need these unique identifiers because TypeScript is compiled into JavaScript and JavaScript does not have support for static types like interfaces. We use types to identify which types need to be injected into a class.
- Declare some classes that implement the interfaces that we previously declared. These classes will be instantiated by the IoC container and for that reasons they require to be decorated using the “@injectable” decorator. We also need to use the “@inject” decorator to indicate which types need to be injected into a class.
- Declare an instance of the “Container” class and then declares some type bindings. A type bindings is a dictionary entry that links an abstraction (type) with an implementation (concrete class).
- Use the IoC container instance previously declared to resolve the “Warrior” type. We declared a type binding between the “Warrior” type and the “Ninja” class so we can expect the IoC container to return an instance of the “Ninja” class. Because the “Ninja” class has a dependency in the “Weapon” and “ThrowableWapon” types and we declared some bindings for those types we can expect instances of the “Katana” and “Shuriken” classes to be instantiated and injected into the “Ninja” class.
- Use the “log” method from the “console” object to check that instances of the Katana” and “Shuriken” has been correctly injected into the “Ninja” instance.
Before running the preceding TypeScript code snippet, we need to compile it into JavaScript. We can to use the “tsc” (TypeScript compiler) command and the project option “-p” to use the compilation options that we previously defined in the “tsconfig.json” file:
$ tsc -p tsconfig.json |
The preceding command should generated a file named “index.js” under the current directory. We can then run the generated JavaScript file using Node.js
$ node index.js |
If everything went well we should see the following text displayed in the console:
cut! hit! |
If we follow the source code we can see how this text comes from methods in the “Katana” and “Shuriken” classes which are invoked through the “Ninja” class. This proves that the “Katana” and “Shuriken” classes have been successfully injected into the “Ninja” class.
Node.js enterprise patterns
What we just saw in the previous section of this article is a basic demo of the core InversifyJS API. When we implement a real world enterprise Node.js application using TypeScript and InversifyJS with Express.js we will end up writing some code that looks as follows:
import * as express from "express"; import { Response, RequestParams, Controller, Get, Post, Put } from "inversify-express-utils"; import { injectable, inject } from "inversify"; import { interfaces } from "./interfaces"; import { Type } from "./types"; import { authorize } from "./middleware"; import { Feature } from "./features";
@injectable() @Controller( "/api/user" authorize({ feature: Feature.UserManagement }) ) class UserController {
@inject(Type.UserRepository) private readonly _userRepository: interfaces.UserRepository; @inject(Type.Logger) private readonly _logger: interfaces.Logger;
@Get("/") public async get( @Request() req: express.Request, @Response() res: express.Response ) { try { this._logger.info(`HTTP ${req.method} ${req.url}`); return await this._userRepository.readAll(); } catch (e) { this._logger.error(`HTTP ERROR ${req.method} ${req.url}`, e); res.status(500).json([]); } }
@Get("/:email") public async getByEmail( @RequestParams("email") email: string, @Request() req: express.Request, @Response() res: express.Response ) { try { this._logger.info(`HTTP ${req.method} ${req.url}`); return await this._userRepository.readAll({ where: { email: email } }); } catch (e) { this._logger.error(`HTTP ERROR ${req.method} ${req.url}`, e); res.status(500).json([]); } }
} |
As we can see in the preceding code snippet, the inversify-express-utils allow us to implement routing, dependency injection and even apply some Express.js middleware using a very declarative and developer friendly API. This is the kind of developer experience that we want to enable thanks to InversifyJS and TypeScript.
Features & Tools
The core InversifyJS has a rich API and supports many use cases and features including support for classes, support for Symbols, container API, controlling the scope of the dependencies, injecting a constant or dynamic value, create your own tag decorators, named bindings, circular dependencies
In top of an extensive list of features, we also want to provide developers with a great user experience and we are working on a serie for side-projects to facilitate the integration of InversifyJS with multiple frameworks and to provide developers with a great development experience : inversify-binding-decorators, inversify-inject-decorators, inversify-express-utils, inversify-restify-utils, inversify-vanillajs-helpers, inversify-tracer, inversify-logger-middleware, inversify-devtools.
Future development
The main focus of the InverisfyJS project is the core library. We want to continue listening the needs of the users of the library and keep adding new features to support those use cases. We also want to ensure that we provide users with utilities to facilitate the integration of InversifyJS with whatever framework they are using.
Summary
InversifyJS is a dependency injection library with a rich set of features and a rich ecosystem. If you wish to learn more about InversifyJS, TypeScript and the dependency injection principle in JavaScript please refer to the following links:
- The official InversifyJS wiki
- The TypeScript hand book
- The current state of dependency injection in JavaScript
This article was originally published in May 2017
Methods & Tools Testmatick.com Software Testing Magazine The Scrum Expert |