Nest.js backend architecture
Author: Mateusz Koteja
Nest (NestJS) is a framework for building efficient, scalable Node.js server-side applications. https://docs.nestjs.com/
Nest.js documentation is great. It's built (just like Nest) around components. You just pick what you need, copy&paste the code, and the job is done.
There is small problem though. Nest.js documentation doesn't tell the programmer how to build the application or structure the code. It provides building blocks, and a mechanism to connect them. There is nothing about application's architecture.
Problem context
Nest provides modules (for dependencies isolation and injection), services, controllers. The simple CatModule/CatService (+ CatEntity)/CatController application, that is presented in Nest documentation (we call it Nest way) is just an example how to connect those blocks. It should not be the final application architecture (which revolves around the idea of module per database entity).
flowchart LR
subgraph CatModule
direction LR
CatController --> CatService --> CatEntity
end
subgraph DogModule
direction LR
DogController --> DogService --> DogEntity
end
DogModule -- getHatedCats --> CatModule -- getHatedDogs --> DogModule
Such style leads to following problems:
-
Modules are tied to a database entity. If I have one module per database entity application's business logic is reflected by the database implementation, while in fact it should be vice-versa. Database implementation should be implementation detail, and module scope should be defined by the business requirements, the domain language. Although determining module's scope is a separate topic (as it is non-trivial task), I can give an example: let's say, that I want to create a chat application. I don't create
MessageModule/TopicModuleor whatever data structure I have. As I want to create chat application, I createChatModulewhich stores things related to chat domain (messages, topics, even files maybe). The data structure should be irrelevant for module's scope. It's much more pleasant to work with, and reason about. I can have people specialized in chat domain, and solving chat problems, that work inside one module scope. Modules don't have to be granular - they can be large, if the domain is extensive. -
Controllers are tied to a database entity via module. When they are crafted for frontend's needs (frontend usually requires some joins between the data) it leads to dependency mess - one module imports another module only to create a controller for the application, not to reflect the business need. Probably
ChatModuledoesn't have to know aboutUserModuleexistance at all, it just needs to storeuserIdnext to a message. It also leads to a question in which module controller should be - sometimes it's ambiguous (especially if we are talking about these data joins). -
Services are tied to a database. As services which usually provide some business logic are tied to a database directly they are hard to test. Either a programmer mocks the database dependency (and mocking tends to be annoying and/or error-prone), or works on actual database (which requires cleaning before/after tests and makes parallel tests impossible). Also, tying service to an ORM manager/repository/active record makes impossible to switch the tool without touching business logic in the service, and creates a hard-dependency on database structure (which again should be an implementation detail).