Layered architecture in NestJS

Layered architecture in NestJS

NestJS utilises an idea of modules. Modules act as isolated DI containers and serve purposes of responsibility separation. It helps to group code of your app by feature. One modules reuse business logic of other modules. Modules can be structured inside in any way you choose, the only important thing – decide on what logic they encapsulate.

But what if we need to reuse, for example, repositories of another module? We will want to export those repositories, but it will break SPR of a feature module. We can also duplicate implementation of a repository.

Actually, I don't like those approaches and I think cross-app layered architecture can help to increase code reusability and preserve "feature modules" approach.

Let's see how it works on example of this blog, which I wrote using NestJS. I have the following feature modules:

  • Authentication module
  • User modules
  • Blog module
  • Files module

But those are not usual NestJS modules. They don't even have *.module.ts files. The idea of feature module that lay in my approach is more about convention and code organisation. The more interesting is what they consist of. Here is where layers come into game.

Every feature module consists of 3 submodule types:

  • Http module
  • Core module
  • Database module

Does it remind you about anything? Yes, it's a usual 3-layers architecture. Let's dive into each module type deeply.

Http module

This type of modules is responsible for presentation.

It includes

  1. Controllers,
  2. Request/Response objects,
  3. Http exception classes,
  4. Decorators/guards,
  5. Other presentation-level stuff.
Example of http module

It depends on

Http modules depend on core modules of any feature modules.

Also this kind modules are the only modules that depend on NestJS framework. Uncle Bob taught us to consider frameworks as features. Here it serves as a feature – I use it for presentation layer to serve my business logic to outer world. I also use NestJS's DI container. That's all. Or not...

Actually there is one tricky thing, that make all my code depend on NestJS. It's DI container. I mark injectable classes using Injectable decorator and inject all dependencies using Inject decorator, which I import from NestJS. 

This problem can be solved by creating a level of of abstraction on NestJS' decorators, but I consider it as excess practice, since mass-replacement of import { Injectable } from '@nestjs/common'; statement will cost you nothing in case if you decide to migrate to another framework.

How to use

Import http modules in app.module.ts

Core module

This type of modules is responsible for business logic.

It includes

  1. Services, which are exported to outer world,
  2. Business-layer exception classes,
  3. Other business-layer related stuff.

Core modules doesn't have its own DTOs, since it uses DTOs of persistence layer. I find this approach convenient, since it doesn't break a rule of downward flow.

Example of core module

It depends on

Core modules depend on database modules of any feature modules.

How to use

Inject them into your http modules or any other presentation-level modules.

Database module

This type of modules is responsible for database manipulations.

It includes

  1. Repositories, which are exported to outer world,
  2. DTOs,
  3. Repository-level exception classes,
  4. Other persistance-level related stuff.
Example of database module

It depends on

Any library you choose to access database.

How to use

Inject them into your core modules of any feature modules.

I hope this article has helped you to decide on how to organise your NestJS project. Feel free to contact me if you want to discuss the described approaches or suggest yours