OOP and Dependency Injection in NextJs
Inversion of Control with InversifyJs in NextJs
Find the code on github, here.
For the past year and a half I've been working with OOP in JS with various frameworks, from AdonisJs to NextJs, but only the past few months I really got the gist of it.
If you are anything like me, you would ask yourself: what's the deal with all of those objects? Why can't I just use functions? Isn't calling methods the same as functions?
Well, as it turns out, OOP can really help you better organize your code and reuse it.
Let's start with some basics.
Classes
A class in js is mainly some syntactic sugar for a constructor function. So instead of writing
function MyClass(name) {
this.name = name;
this.getName = function() {
return this.name;
}
}
const myClass = new MyClass('Mihai');
console.log(myClass.getName()); // Mihai
You write
class MyClass {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
const myClass = new MyClass('Mihai');
console.log(myClass.getName()); // Mihai
And if you are using Typescript, you can even drop the initialization in the constructor because if you use theparameter properties syntax.
class MyClass {
constructor(public name: string) {}
// same as above
}
Anyways, this is all fine and dandy as whenever you need a new class, you just call new MyClass()
and voila, you can use the methods that the class has to offer.
However, in complex applications, things get complicated fast.
You won't just use classes that accept some basic properties, but you will use classes that use other classes, like a service class, using a repository class, which is using an api gateway class.
For example:
class MyService {
constructor(private repository: MyRepositoryInterface) {}
public myMethod() {
return this.repository.getSomething();
}
}
class MyRepository implements MyRepositoryInterface {
constructor (private apiGateway: ApiGatewayInterface) {}
public getSomething() {
return this.apiGateway.get('/some-url');
}
}
class ApiGateway implements ApiGatewayInterface {
constructor (private baseUrl: string) {}
public get(url: string) {
return fetch(`${this.baseUrl}/${url}`);
}
}
// Whenever you need an instance of myService, you need to do the below
const apiGateway = new ApiGateway('http://www.example.com');
const myRepository = new MyRepository(apiGateway);
const myService = new MyService(myRepository);
myService.getSomething();
So, whenever you need the myService class in a component, you will need to import all of the other dependency classes, instantiate them and then instantiate myService with the class(or classes) that it needs.
This, of course, is not ok.
Enter InversifyJs
IMPORTANT install steps!
You need the following packages:
npm i --save inversify reflect-metadata
npm i --save-dev @babel/core @babel/plugin-proposal-decorators babel-plugin-transform-typescript-metadata
You can find more info about the last 3 packages and the problem they prevent here.
Then add a babel.config.js
file with the following:
module.exports = {
presets: ['next/babel'],
plugins: [
'babel-plugin-transform-typescript-metadata',
['@babel/plugin-proposal-decorators', { legacy: true }]
]
};
Then, in your tsconfig.json
add the following:
{
"compilerOptions": {
...
"types": [
"reflect-metadata"
],
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Now the installation is complete.
This package provides you with a container on which you can bind your classes and then import them from there.
It also provides you with some classes metadata that make them @injectable or @inject the dependencies they need.
For example, you can create your classes like so:
import {injectable, inject} from 'inversifyJs';
import {TYPES} from '.types.ts';
@injectable
class MyService {
constructor(@inject(TYPES.MyRepository) private repository: MyRepositoryInterface) {}
public myMethod() {
return this.repository.getSomething();
}
}
@injectable
class MyRepository implements MyRepositoryInterface {
constructor (@inject(TYPES.ApiGateway) private apiGateway: ApiGatewayInterface) {}
public getSomething() {
return this.apiGateway.get('/some-url');
}
}
Where types.ts is defined as
export const TYPES = {
ApiGateway: Symbol.for('ApiGateway'),
MyRepository: Symbol.for('MyRepository'),
MyService: Symbol.for('MyService')
};
Then, you need a container/index.ts file where you actually define the container:
import { Container } from 'inversify';
import { TYPES } from './types';
import ApiGateway from 'app/shared/api.gateway';
import { ApiGatewayInterface } from 'app/shared/api.gateway.interface';
import { MyRepositoryInterface } from './interfaces/my.repository.interface';
import { MyRepository} from './repositories/my.repository';
import { MyServiceInterface } from './interfaces/my.service.interface';
import { MyService} from './services/my.service';
const appContainer = new Container();
appContainer.bind<ApiGatewayInterface>(TYPES.ApiGateway).toConstantValue(new ApiGateway('http://container-link.ccoom'));
appContainer.bind<MyRepositoryInterface>(TYPES.MyRepository).to(MyRepository);
appContainer.bind<MyServiceInterface>(TYPES.MyService).to(MyService);
export { appContainer };
What you are doing here is you bind to a symbol of a certain interface, a certain class.
This way, when you inject that symbol in your classes, inversifyJs will know to provide you with an instance of the bound class.
NOTE THE ApiGateway is bound as a constant value. That is because this class needs to be instantiated with a value and then used with this value in all of the other classes. The same way it would be for a config.
After this, you no longer have to instantiate the classes in your components, but you can do the following:
import { appContainer } from 'app/client/container';
const Home: NextPage = (props: any) => {
const myService= appContainer.get<MyServiceInterface>(TYPES.MyService);
return (<div></div>)
}
So, goodbye to instantiating all of the classes. You just get the class that you need from the container and that's it.
If you want to add a provider for this container and create a hook, you can try this implementation here.
That's all folks!