May 14, 2024 · 5 min read · Backend Engineering and Architecture
Using NestJS in Large Backend Applications
NestJS gave our TypeScript services a shared structure. Its value became clearer when several engineers needed to understand, review and change the same backend.
I’ve used NestJS in integration platforms, SaaS products, and internal systems. The obvious benefit is structure. Controllers, providers, modules, validation, and dependency injection all have recognizable places. For me, the value appears later, when the project isn’t small anymore and more engineers are changing it.
A Shared Project Structure
On one integration platform, we had services responsible for receiving requests, applying business rules, persisting operation state, and communicating with external systems. Without agreed boundaries, Node.js makes it easy for a project to become a collection of files that each combine a little routing, database access, and business logic. The application can still work, but understanding a change becomes harder as the team and codebase grow. NestJS gave us a common structure. An engineer opening a module for the first time had a good idea of where the HTTP boundary was, how dependencies were connected and where the application behavior should be. That predictability made reviews and onboarding easier. We spent less time discussing where a file belonged and more time discussing whether the behavior was correct.
The same structure also worked for background processing. A worker didn’t have an HTTP controller, but it could still use the application services, repositories and integration clients through the same dependency injection setup. This helped us avoid creating one version of the business flow for the API and another inside a queue consumer. The transport changed while the important behavior remained in a place both entry points could call.
We had to be careful with shared modules. Making a provider global was convenient when many parts of the application needed it, but it also made dependencies less visible and allowed modules to reach into each other without a clear contract. We kept infrastructure such as configuration and logging shared where it made sense, while business capabilities exported a smaller set of services instead of every internal provider.
Modules and Boundaries
NestJS doesn’t save a project from poor architecture. I’ve seen large services hide inside perfectly valid modules, and dependency injection can still hide a graph that nobody understands. The framework gives us the building blocks, but the team still needs to organize them around the business domain. I prefer modules that represent capabilities the system provides rather than folders for technical categories. A module named after an operation or business concept tells me more than a global folder containing every controller in the application. I’m not looking for perfect isolation; I want a normal change to require a reasonable amount of context. TypeScript makes contracts visible during development, although a large interface called Payload can be fully typed and still communicate very little about the business operation.
We used specific request and response models, validated external input, and translated transport data before it reached important business rules. This made the code less dependent on the exact shape of an HTTP request or third-party response. The types were more useful when they described the domain instead of only describing the JSON.
External integrations made that separation especially useful. Third-party payloads changed for reasons our domain didn’t control, and some of them used optional fields or status names that didn’t match our own model. We mapped those responses at the boundary instead of passing them through the application. When an integration changed, the compiler helped us find the mapping and contract, while the rest of the workflow could continue working with the same internal concepts.
Validation pipes and decorators reduced repeated code at the request boundary, but we didn’t treat request validation as the complete business validation. A DTO could confirm that a date existed and had the correct format. It couldn’t decide by itself whether that date was allowed for the operation or whether the current user could make the change. Those decisions stayed in the application flow where they could also be used outside HTTP.
Team Conventions
The conventions also saved us from repeating the same discussions about validation, errors and database access in every module. We still disagreed about designs and NestJS didn’t prevent large services from appearing, but at least an engineer could open an unfamiliar area and recognize the basic structure. I wouldn’t choose it for every Node.js application and a small service may not need all of it. On the projects where I used it, the useful part wasn’t having a perfect module tree. It was being able to find the request boundary and follow the behavior without learning a different architecture for every feature.
Testing exposed where the structure was helping and where it was only decoration. If a service needed a large NestJS testing module and many unrelated mocks to verify one business decision, the dependencies were probably too broad. We kept framework-level tests for routing, validation and integration between modules, but the important rules were easier to work with when they could be tested through smaller services.
As the team grew, we documented a few conventions rather than trying to create a rule for every file. We agreed on how modules exposed behavior, where external responses were translated and how errors reached the API. There were still exceptions, but new features started from the same general shape. NestJS gave us the vocabulary for that conversation; the team still had to decide what each boundary meant for our application.