DEV Community

Cover image for Implementing Onion architecture in NestJS
Amro
Amro

Posted on

Implementing Onion architecture in NestJS

What’s Onion architecture?

As shown in the picture, onion architecture is a way to structure the code by dividing it into domain-driven design layers. each layer can only access the layer below it throw its interfaces, and then using the dependency inversion principle each interface will be replaced with its class.

onion architecture

Onion Architecture leads to more maintainable applications since it emphasizes separation of concerns throughout the system.” Jeffery Palermo

Why apply Onion Architecture in NestJs projects?

NestJs is a service-side framework based on NodeJs. NestJs has many built-in features but most importantly for us now is the Dependency Injection and the possibility to add Dependency inversion which is what we need to apply the Onion Architecture.


Building simple server-side Blog:

In the rest of the article, I’ll try to explain the NestJs implementation using a simple blog project.

I’ll assume most of the article readers already know Nestjs so I’ll focus on the architecture in the code example.

Project layers in NestJs

project layers in nestjs

  • Domain entities: in the core of our application we have the domain, according to the Domain-driven design(DDD), we should focus our implementation around our Domain and all other layers are built around it.

In our case, the Domain Entity is just the Article, so let’s do its interface:


export interface IArticle {
  id: number;
  body: string;
  title : string;
}

Enter fullscreen mode Exit fullscreen mode
  • Repository: is the layer that participates in the Domain Entity, like getting or deleting entity object but it has to abstract away database and infrastructure details, so it’ll work with any kind of database.

so let’s build ArticleRepository:

export interface IArticleRepository {
  get(id: number): Promise<IArticle>;
  delete(id: number): Promise<void>;
  save(input: IArticle): Promise<void>;
  update(input: IArticle): Promise<IArticle>;
}
Enter fullscreen mode Exit fullscreen mode

As the Repository interface most likely will have the same functions for all Entities as it should mainly perform these abstract functions, I recommend using Generics that takes the Entity type as parameter to have general Repository

export interface IRepository<T> {
  get(id: number): Promise<T>;
  delete(id: number): Promise<void>;
  save(input: T): Promise<void>;
  update(input: T): Promise<T>;
}
Enter fullscreen mode Exit fullscreen mode
  • Service: here we implement the use cases, it participates in the Repository to get the data it needs

in our case we need a service to get an article by id and count article characters:

export interface IArticleService {
  getArticle(id: number): Promise<IArticle>;
  getArticleLength(id: number): Promise<number>;
}
Enter fullscreen mode Exit fullscreen mode
  • Controller: for the sake of simplification I made the controller the first layer, hence NestJs is a server-side framework. But normally in literature first layer suppose to be the UI or test.

As Controller is our first layer and we won’t use it as dependencies somewhere else so no need to write an interface for it and we can implement it directly.

import { Controller, Get, Param } from '@nestjs/common';
import { IArticle } from './article.interface';
import { IArticleService } from './articleService.interface';

@Controller({ path: 'article' })
export class ArticleController {
  constructor(private readonly service: IArticleService) {}

  @Get(':id')
  async article(@Param() params): Promise<IArticle> {
    return this.service.getArticle(params.id);
  }
}
Enter fullscreen mode Exit fullscreen mode

So are we done?

ofc not yet, we just wrote the interfaces but still didn’t write the actual implementation of them, and then replace the interface with the class in the run time.
In the rest of the article, I’ll implement one of the Classes(ArticleService) and the rest will be the same.


Implementing ArticleService


@Injectable()
export class ArticleService implements IArticleService {
  constructor(
private readonly repository: IRepository<IArticle>) {}

  async getArticleLength(id: number) {
    const article = await this.repository.get(id);
    return article.body.length;
  }

  getArticle(id: number) {
    return this.repository.get(id);
  }
}
Enter fullscreen mode Exit fullscreen mode
  • must add @Injectable() so NestJs can inject it later in a class as dependency.
  • the class implements the interface to make sure the class has the same functions
  • as we see here ArticleService has dependencies like the Controller but this time its dependency is IRepository

Dependency inversion( replacing the Interface by the Class):

In the ArticleModule we can specify a string token for each class and use this token when we use the class as a dependency.

so let’s apply that with ArticleService, giving it a token in ArticleModule:

@Module({
  controllers: [ArticleController],
  providers: [
    {
      provide: 'ARTICLE_SERVICE_TOKEN',
      useClass: ArticleService,
    },
  ],
})
export class ArticleModule {}
Enter fullscreen mode Exit fullscreen mode

and using this token in the Controller to get the Class in run it:


@Controller({ path: 'article' })
export class ArticleController {
  constructor(
    @Inject('ARTICLE_SERVICE_TOKEN')
    private readonly service: IArticleService,
  ) {}
  ...
}

Enter fullscreen mode Exit fullscreen mode

Note: as strings are bound to errors, it’s best practice to assign the token string to a const variable and export it from the IArticleService file and then use it instead of the string directly:

export const ARTICLE_SERVICE_TOKEN = 'ARTICLE_SERVICE_TOKEN';

export interface IArticleService {
  getArticle(id: number): Promise<IArticle>;
  getArticleLength(id: number): Promise<number>;
}
Enter fullscreen mode Exit fullscreen mode

— that’s it 🎉 now imagine if we needed to change any class, we’ll just add the new class to the useClass parameter in the Module without having to change the controller implementation.
depenciey inversion spongbob


Don’t be so radical about it:

In the end, Onion architecture was made to make the development process easier, so don’t try to force it everywhere where it does not make much sense due to some libraries limitation or other reasons.


Thanks for writing till the end and wish we meet in another article. take care!

Discussion (0)