Using Injection-Js with Express and Typescript
I wrote this tutorial while using ‘injection-js’ on a Node project. The reason was that this framework was part of the original Angular project, and all the documentation I could find online assumed an Angular (rather than a Node) application.
For this demonstration we will build a simple IMDB search engine using the Google API. I will be skipping most of the UI and Google API parts, such as registering for an API key, to focus on Dependency Injection (DI )using the ‘injection-js’ framework (you can always read the codebase directly for the rest).
To simplify this tutorial even further and keep it focused on DI, we’ll start with a basic project described in detail in this excellent tutorial. Create an npm project and add the basic dependencies
npm init --yes
npm i express ts-node typescript tslib @types/express @types/node
Then include the ‘injection-js’ library itself:
npm i injection-js
Typescript needs a config file (named tsconfig.json) in order to compile:
{
"compilerOptions": {
"experimentalDecorators": true,
"sourceMap": true,
"target": "es6",
"module": "commonjs",
"outDir": "./dist",
"baseUrl": "./src"
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules"
]
}
Notice the experimentalDecorators variable? Injection-js uses decorators to add classes in the DI container. Then, in package.json, add the scripts as:
"scripts": {
"build": "tsc -p .",
"dev": "ts-node src/server.ts",
"start": "npm build && ts-node dist/server.js"
}
The “dev” script will run typescript directly while the build will compile it to javascript and place it in a dist/ folder. For the most part, and since this is tutorial, we’ll be using “dev.”
To start an express server, create a server.ts file and add the basic instructions:
import * as express from 'express';const port = process.env.PORT || 3000;
const app = express();// Serve the homepage
app.get('/', (req, res) => {
res.sendFile('home.html', { root: __dirname });
});// Start
app.listen(port, () => {
console.log(`App listening on the http://localhost:${port}`);
});
We will create a basic HTML with a search bar. I included Bootstrap and jQuery, both of which are loaded through a CDN so you don’t need to install anything. I won’t post it here so please look into the project. Now, for the “meat.”
Dependency Injection
We will create a service that makes the request to Google and returns the search results. The service will need the API_TOKEN obtained by Google, and since we might choose to pass this as an environmental variable in needs to be passed as a parameter, so why not inject it? The framework can inject classes without much trouble because it assumes that a class will be instantiated. Our token however is a string, so we must tell the framework how to resolve it. For this we create a token in a separate file called injection-tokens.ts.
import { InjectionToken } from "injection-js";export const API_TOKEN = new InjectionToken<any>('API_TOKEN');
In the SearchService class we can now inject the token in the constructor.
import { Inject } from "injection-js";
import { API_TOKEN } from "./injection-tokens";class SearchService {
constructor(
@Inject(API_TOKEN) private api_token) {
}
search(term: string) {
// look into the repo for the implementation
}
}
All we need now is to provide for this token so that ‘inject-js’ knows what to do when encountering it.
const api_token = process.env.API_TOKEN || 'AIzaS...';const providers = [
{ provide: API_TOKEN, useValue: api_token },
];const injector = ReflectiveInjector.resolveAndCreate(providers);
const service = injector.get(SearchService);
What we now want is to create a controller class that is going to use the service. The service therefore needs to be an injectable itself.
import { Injectable } from "injection-js";@Injectable()
class SearchService { ...
We will add a new token to the injection-tokens.ts file.
export const SEARCH_SRV = new InjectionToken<any>('SEARCH_SRV');
and the token to the list of provides, this time using the useClass:
const providers = [
{ provide: API_TOKEN, useValue: api_token },
{ provide: SEARCH_SRV, useClass: SearchService },
];
This suggests to the framework that a new instance of the SearchService must be created every-time the token is injected (rather that a constant value served as in the api_token above).
import { Inject } from "injection-js";
import { SEARCH_SRV } from "./injection-tokens";class HomeController {
constructor(
@Inject(SEARCH_SRV) private service: SearchService) {
}
search(term: string) {
return this.service.search(term)
}
}
The HomeController will not be injected, and so it can be added to the list of providers directly as a class. The framework will assume that this is equivalent to a ‘useClass’ provider object. The complete providers for this project are:
import { Provider, ReflectiveInjector } from "injection-js";const providers = [
{ provide: API_TOKEN, useValue: api_token },
{ provide: SEARCH_SRV, useClass: SearchService },
HomeController
] as Provider[];const injector = ReflectiveInjector.resolveAndCreate(providers);
const controller = injector.get(HomeController);
We should have a controller that has a service that has a token, all wired-up for us automatically via DI.