NodeJS is one of the technologies that is dominating the software development market. But as almost every technology available out there, the most critical part is not about the technology itself but how we use it to accomplish our goals. Therefore, this post tries to gather all the experience gained working with NodeJS to define a good and tested architecture, based mostly in concepts like Clean Architecture and Good practices, so that you’ll be able to follow it and design your backend in a way that it can adapt to most projects and scale in an appropriate way.
Important ⚠️
This is not the ultimate guide to design the best applications. There’s surely a lot of improvements that can be applied, specially regarding to the needs of each project. But I think you can take advantage of my experience testing this architecture to avoid some problems and headaches while being in the middle of a project that has evolved in an unexpected way.
Popular problems with NodeJS architectures
Let’s start designing our own architecture. Many of the concepts can be applied to other technologies such as .NET Core, Phyton, but let’s focus on this one. We will choose Express for the API and Mongoose for MongoDB. However, many of the concepts used in this section will be transparent to the technology stack used. The code will be written in ES5 for further understanding. A basic NodeJS server example can be seen in this codesandbox, which basically has a resource to allow us to create a new user:
This server has several good ideas behind. It has separated layers for handling API requests and db queries. Separated files for modules of REST resources and also for db modules (although just a single module of users management has been added for this example). It looks like it can escalate smoothly as the quantity and complexity of resources increase, but that’s not quite true. Let’s assume that we have to add a new resource in our server to create a group. A group is designed to contain users. But our business logic determines that a group should create a default user that will be in charge of managing the group. First steps are quite easy to implement, as we can see here:
But how are we going to reuse our previous code for creating the user?
Importing the method in routes layer is annoying, because we need to start passing express linked objects (req, res, next) from one place to another. Importing the method in db layer is inaccurate because we’re missing strategically added functionality in the routes layer.
Let’s consider that we are going to add some unit tests (because of course, we want test our code) to our current functionality.
But …
- Tests for our routes domain wouldn’t be a bit express-linked?
- If we move to a, for example, GraphQLAPI, will we have to rewrite them all?
- How would you solve these problems?
- Splitting the current methods?
- Creating a new layer? Maybe creating several new layers?
Do not reinvent the wheel. Clean Architecture
I’m not unveiling a trailblazer solution. I’m just evaluated the best current architecture models available right now and adapted them to my needs.
So we started analysing one of the most known architecture pattern, the one proposed by Uncle Bob some years ago, called Clean Architecture. It supports the idea of making the model independent from the framework, libraries, dbs, etc. by creating an intermediate layer called Interface adapters. He also remarks the necessity of the code to be easily testable, and how this kind of architecture will allow us to create unit tests of the code not linked to any external technological element. So following those rules we split all the business logic from our API framework, creating a new domain layer:
Our domain code is reusable and independent from our API framework
The code is also easily testable and tests can survive through frameworks and libraries changes. And we can see in the final example version how the core functionality remains unchanged, although we have added a new GraphQL based API that makes use of the same domain methods. On top of that, we have created some unit tests that assure the correct functioning of our business logic autonomously, whatever the API framework is used:
Further improvements
As we have stated previously, this is just a base architecture on top of which you should adapt your project’s needs. Also, as I don’t want this post to be humongous, I’m not going to detail any of the followings improvements, but it’s the duty of the advanced reader to take this issues also in consideration.
- Improve API adapters definition: Our REST API adapter unties our business logic from resources, requests, responses, and other REST stuff. But a more complete layer should include http codes mapping, i18n messages, and everything we need to exploit the full potential of the API protocol
- Add adapter layer to db: In the same way we have split our API technology from our code, we should do the same with the db layer to make our application utterly independent of the db technology we are using. Likewise, we should add an adapter for every external framework we want to isolate from our domain code.
- Define domain entities: Some Clean Architecture models talk about defining the core entities in our business logic and abstract them into a inner layer. That may depend on your application, but it’s something you should definitely consider if you are going to have some well defined entities along your application logic.
Ready for facing the challenge of having a scalable NodeJS server prepared to adapt every situation?
Source: dev