From chaos to clarity: taming a growing codebase with Nest.js

Ihan Dilnath
Ascentic Technology
10 min readOct 8, 2023

Are you a Backend developer grappling with inconsistent patterns and a lack of modularity in your Node.js/Express.js codebases? In this article, let’s discover how Nest.js can offer a standardized solution to tackle these challenges and improve codebase clarity and maintainability. I will share my experience and reasoning why Nest.js might be the framework for you when building backend web and server-side applications for SaaS or enterprise systems, addressing the issues above and others.

What is Nest.js?

In case you’re not familiar Nest.js (or simply Nest) is an opinionated, open-source framework for building Node.js backend web and server-side applications. While Express.js considers itself a framework, Nest uses Express under the hood (by default) and operates at a higher level of abstraction. Nest is akin to frameworks like Spring Boot and ASP.NET Core in Java and .NET ecosystems. It comes with “batteries included” with its dependency injection and Inversion of Control (IoC) container, an integrated HTTP server (using Express.js or Fastify), middleware, database integrations, authentication and authorization, API documentation and more.

Opinionated structure

“…while plenty of superb libraries, helpers, and tools exist for Node (and server-side JavaScript), none of them effectively solve the main problem of — Architecture. Nest provides an out-of-the-box application architecture which allows developers and teams to create highly testable, scalable, loosely coupled, and easily maintainable applications.”Nest.js docs

The main philosophy behind Nest is its architecture. Software architecture represents the significant design decisions of a system based on certain opinions. “Modules” are one of those opinions of Nest.

Overview of Modules in Nest (Source: Modules | NestJS — A progressive Node.js framework)

Modules in Nest dictate how code is organized and what dependencies are provided or injected into those modules. Modules can be either Feature modules or Shared modules, where Feature modules commonly encapsulate the Controller-Service-Repository (or Model) pattern while components like database configuration and other utilities are commonly encapsulated in Shared modules. So what?

Use Modules to structure and organize your code in a consistent way

One attribute that affects consistency in codebases is how code is structured and organized. My opinion is Modules in Nest are a great way to structure and organize code in Express applications. Is structure and organization merely how you name your folders, how you name your source files and how you organize those folders into source files? I think structure and code are much more than that, and should most importantly be about Screaming architecture, cohesion and coupling. These aspects are tenets of Vertical slice architecture and Clean architecture which are some of the popular backend web application architectures and are easily implemented using Modules in Nest. When you take Vertical slice architecture in particular, you can use Modules in Nest as an abstraction for the vertical/feature slices of your application.

This modularity plays an important role in another aspect of Nest: dependency injection.

Dependency injection

Nest helps achieve dependency inversion through its dependency injection (DI) implementation. Why is that useful to your team and codebase when you could use plain require/ES6 import statements to directly import, use and depend on other pieces of code?

Dependency injection in Nest makes it easy to write tests

Because DI makes parts of your code independent and testable. Without it, you have to resort to monkey-patching, quirky import behaviours and experimental features with complicated setups using tools like Rewire and Jest. This is by no means a criticism of Jest; Jest is my choice of testing library even in Node.js.

“Swiss army knife” for injecting all your dependencies

DI in Nest is also flexible and powerful at the same time. It supports injecting dependencies using class-based tokens, non-class-based tokens and factory functions. It is capable of delaying application startup by resolving modules with asynchronous initialization. For example, a configuration module that has to fetch the configuration from a remote service. In addition, Nest’s dependency injection also features dynamic modules to configure modules at run-time and injection scopes to configure how dependencies are resolved and shared across incoming requests and components.

Incremental adoption

Come over to Nest for the Dependency injection, stay for the bells and whistles

Along with Modules, dependency injection is something you get out-of-the-box with Nest. You can only choose to use Modules and dependency injection to start adopting Nest in your projects. You can read more on Standalone applications in Nest docs.

This is an approach I have adopted in one of my recent projects. I broke off certain features, vertical slices, and shared configurations of the codebase into Nest Modules one by one while passing the existing Express Application object to Nest. Nest takes care of binding the newly converted Nest modules to Express routes. Thus, it extends the Express application without affecting any existing code or routes. I will write a step-by-step migration guide from Express to Nest in future. A framework like Nest that allows for incremental adoption gives you and your team time and opportunity to pick and address problems one by one while you get better accustomed to the framework. None of your efforts go in vain.

Separation of concerns

Express handles routing and middleware for you and leaves you great flexibility and freedom in how you wish to separate concerns in your application, business and data logic layers. Express does its job well and gets out of your way when it comes to other parts of your application. You can separate concerns in the other parts of the application within the full scope of what JavaScript/ECMAScript allows you to. You can use classes, or you can use modules and functions to encapsulate your logic. You can do data validations in the same class or function where you handle the rest of the application and business logic, or you could do data validations in a separate Express middleware function handler. You can even have all the logic for a single endpoint in a single Express middleware function handler. It’s great, isn’t it? It really is. However, ideally, you want to strike a balance between flexibility and rigidity.

Having no recipe can be a recipe itself, a recipe for disaster.

Over time you might find a multitude of ways to do the same thing in various parts of your codebase. You run into several issues when the separation of concerns is inconsistent across a codebase:

  • Predictability — it becomes harder to predict what parts of the code you need to change or even debug for troubleshooting.
  • Maintainability — it becomes harder to implement changes to your codebase because they are implemented using different patterns in different parts of your codebase. For example, you want to introduce role-based authorization, but parts of your codebase handle authorization in Express middleware function handlers while other parts handle it in each “controller” component.
  • Code reusability — when a codebase does not follow a common set of abstractions it becomes challenging to reuse code in future. It can lead to further code duplication.
  • Tech debt — further code duplication can accumulate tech debt over time.
  • Testing — when you write unit tests and integration tests, you might need to mock a significant number of dependencies in certain units or components of the codebase.

Order from the menu or BYOA (Bring your own abstractions) with Nest

My opinion is Nest gives you a good balance of flexibility and rigidity through its useful abstractions that cover common use cases in backend web and server-side applications for SaaS and enterprise systems. Nest provides several abstractions such as:

  • Controllers for handling incoming requests, responses and application layer logic.
  • Services that are delegated by Controllers to handle domain logic.
  • Guards to handle the responsibility of authorization.
  • Interceptors that can implement any cross-cutting concerns or Aspect-oriented programming.
  • Pipes to perform either data transformations or validations for incoming and outgoing data.
  • Exception filters for processing any unhandled errors across the application in a standard way.

However, Nest still gives you the freedom to implement any abstractions of your own and declare them as a Provider so you can use them in other Nest components.

Platform-agnostic and Microservices-ready

Microservices have been all the rage in recent years both figuratively and literally for reasons good and bad. What I learned from my industry experience with microservices is that it is something you evolve towards over time if need be. It certainly isn’t something you should start off with. Solve the problems you have today first. Unless you are rewriting a matured system, my judgment is that you should start off with a Modular Monolith. However, it certainly isn’t in vain to consider what Nest has to offer if you need to scale to microservices one day.

Nest will be there for you when you make the step up to Microservices

If you write your application to serve HTTP requests today, Nest makes it near-seamless to move towards Event-driven or message-driven microservices tomorrow. This is because Nest is platform-agnostic and provides abstractions over protocols like HTTP, GraphQL, WebSockets, Kafka, or gRPC. It could even run your application as a CLI application or Cron job. Nest affords you more time to prioritize writing application and business layers logic.

Another aspect that makes Nest microservice-ready is that it has first-class support for implementing several microservice patterns and best practices. The CQRS module is something I haven’t used yet but found intriguing. It provides an implementation of CQRS based on event buses and the mediator pattern, similar to MediatR in the .NET ecosystem. It even accommodates Sagas using RxJS under the hood so you can execute workflows that compose several commands or perform rollbacks. In addition to CQRS, Nest features Health checks, Rate Limiting, Swagger docs, Pub/Sub using Redis, and more.

JavaScript support and strong type hinting

If you write Express applications in JavaScript, Nest allows you to keep writing them in JavaScript. Although Nest is built with Typescript it has full support for JavaScript. You are still able to leverage type hinting using type definitions in Nest when you use editors like VSCode that have integrated type checking, so you do not need to write or compile code in Typescript to make use of type definitions.

Ever-evolving

There’s more to software frameworks than development alone. They also have a socio-technical system around them. A popular open-source framework is usually backed by a company or community. Nest is very popular and actively maintained by both its community and Trilon. This means you get up-to-date documentation, and the framework keeps evolving till it reaches maintenance mode or end of life. It reduces the effort to maintain internal documentation and onboard new team members into the codebase. It even makes the learning curve of a new codebase gentler for them.

Enterprises that use Nest (Source: https://enterprise.nestjs.com/)

To the knowledge of the maintainers, Nest is also used by these enterprises. Trilon, the consultancy behind Nest provides Enterprise support to such companies.

When should you not use Nest?

You’re building an application on an extreme time constraint such as a hackathon.

There are many Express boilerplates that make certain design and technology decisions upfront for you so you can build something quickly and have it up and running.

You’re building a throwaway PoC

Any time and effort you invest in writing clean and modularized code with Nest goes to the bin. That’s what a throwaway PoC literally means.

You don’t know how you will build something (low-level design)

It is usually easier to predict what is a suitable code structure for a CRUD or SaaS application, what kind of problems in code you could encounter and design patterns that will solve them for you. You will also be guided by any experience from building similar applications previously.

However, there might be applications or projects where it’s hard to anticipate because of domain complexity, or significantly and constantly changing requirements. It might even be a JS/TS library that you are building.

Learning curve

As it is with any framework there is a learning curve. However, if you or your team are competent with JavaScript and come from a previous background in Spring Boot or ASP .NET Core or frameworks similar to those, they might find the transition smoother. If they work with Angular on the front end, they may find many features of Nest such as Modules and Decorators familiar as Nest is heavily inspired by Angular. Nest also has plenty of learning resources including its official docs that’s comprehensive but easy to navigate and has practical examples in each section, plenty of online courses including official Nest courses from the creators of Nest and samples in the official Nest GitHub Repo. There’s also the Awesome NestJS repo on GitHub that has an amazing compilation of everything Nest from more learning resources to examples, boilerplates and libraries.

You already use a framework like Loopback, Adonis or Sails

Do not let “shiny object syndrome” affect you. If you already use a framework stick to it as long as it solves the problems you have today, and it works well for you. Once it doesn’t work well for you anymore or you need to rewrite the application, I hope you consider Nest at that point :)

Conclusion

In this blog post, I discussed the lack of consistency and modularity you may find in Node.js/Express.js codebases as teams and codebases scale and suggested using Nest.js as a solution. In Nest, we explored the opinionated structure with modules, the benefits of dependency injection, how you could incrementally adopt it into your existing projects, its common abstractions for separation of concerns, and its support for platform-agnostic and microservices-ready development. Additionally, Nest also has JavaScript support and strong type hinting and is ever-evolving. By all means, Nest is not a “one size fits all” solution, but it’s great for building backend web and server-side applications in Node.

If you enjoyed this article, please follow me on Medium for future blog posts and follow me on Twitter at https://twitter.com/@ihanblogstech for other content that I share and comment on.

--

--

Ihan Dilnath
Ascentic Technology

Senior Software Engineer | Backend-focused Full stack developer | DDD enthusiast