If you're a web developer and have worked with Node.js, you're probably familiar with Express.js. It's a very popular and flexible library, but as projects grow, it can become difficult to manage. This is where NestJS comes in—a progressive framework for building server-side applications with TypeScript.
What is NestJS
NestJS is a Node.js framework based on TypeScript that uses modular architecture and incorporates concepts from object-oriented programming (OOP), functional programming (FP), and reactive programming (RP). It's designed to be scalable and structured, making it ideal for large projects.
How NestJS works
NestJS is built on the following key components:
Modules
Modules are the core of NestJS. They help organize the application into reusable and maintainable parts.
import { Module } from "@nestjs/common";
import { UsersModule } from "./users/users.module";
@Module({
imports: [UsersModule],
})
export class AppModule {}
Controllers
They handle HTTP requests and return the necessary responses.
import { Controller, Get } from "@nestjs/common";
@Controller("users")
export class UsersController {
@Get()
findAll() {
return "List of users";
}
}
Services
They contain the business logic and can be injected into controllers.
import { Injectable } from "@nestjs/common";
@Injectable()
export class UsersService {
getUsers() {
return ["John", "Maria", "Carlos"];
}
}
Decorators
These are functions that help structure the code declaratively. Some examples include @Controller(), @Get(), @Injectable(), etc.
Advantages of NestJS
- Scalable structure: Organizes code in a modular and clean way.
- Built-in TypeScript: Reduces errors and improves maintainability.
- Dependency injection: Facilitates code reuse and modularization.
- Compatible with Express and Fastify: Choose your preferred HTTP engine.
- Large community and documentation: Well-supported and constantly growing.
Considerations
- Learning curve: If you're coming from Express.js, adapting to the modular structure may take time.
- Initial complexity: For small projects, NestJS might feel like overkill.
- Heavy use of decorators: Not all developers are familiar with them.
My experience with NestJS
The first time I saw NestJS, it felt intimidating. The initial structure includes a lot of boilerplate code, which can be overwhelming for newcomers. Also, its resemblance to Angular (as it is inspired by it) made me question whether it was the right choice for me.
However, as I started using it, I realized that its initial complexity translates into a great advantage in the long run. Almost every feature you might need has probably already been considered by the NestJS team. From validation with class-validator, database integrations like PostgreSQL and MongoDB, caching, logging, file uploads, and OpenAPI documentation, to support for WebSockets and microservices with Redis, Kafka, RabbitMQ, and more. Personally, I’ve used it with Kafka, and the integration is flawless. Additionally, its CLI is a powerful tool that speeds up the creation of new components.
I’ve seen a lot of criticism toward NestJS, especially regarding performance, comparing it to Express.js. But these comparisons don’t make sense: NestJS runs on top of Express.js (or Fastify if you prefer) and adds functionalities that streamline development. It’s like building an application with Express.js and manually adding dozens of packages to achieve the same features that NestJS already provides out of the box.
Ultimately, the decision to use NestJS depends on the product's needs and goals. If you need maximum flexibility and speed, Express.js is a great option. If you need an application maintained by several people with clear conventions from the start, NestJS can be a good choice. In my case, it has become one of my preferred options for backend development.
Conclusion
If you're looking for a framework to build scalable, maintainable backend applications with a well-defined structure, NestJS is a solid option. Its TypeScript integration, modularity, and ecosystem make it a mature alternative to Express.js.
Before adopting it in production
I would not choose NestJS only because it offers many integrations. First, I would evaluate team size, expected product lifespan, TypeScript experience, and operational needs. A small API may benefit more from Express's directness; a backend maintained by several people usually recovers the cost of Nest conventions sooner.
I would prototype one representative feature instead of an artificial CRUD. It should include authentication, validation, persistence, a business error, and an external dependency. That reveals whether guards, pipes, providers, and modules help with the problems the product will actually contain.
I would also check for these risks:
- Global providers that hide dependencies.
- Modules that export nearly everything and stop encapsulating.
- Large services with dozens of responsibilities.
- Decorators applied by habit without a clear policy.
- Tests that recreate the whole container to verify one simple function.
NestJS works best when the team agrees on what belongs to the framework and what belongs to the domain. A guard can handle cross-cutting authentication; a rule such as “only the owner may cancel a pending invoice” is often clearer near the use case. That separation prevents the business from becoming a collection of decorators.
Finally, I would measure startup time, memory, latency, and developer experience using a real flow. The goal is not to prove Nest fast or slow in the abstract. It is to know whether its cost matters compared with database, network, and team productivity.
An adoption path that avoids ceremony
I would start with one business module and keep AppModule as composition. DTOs would validate the HTTP contract, while important rules would live in an operation that can be tested without starting a server. A repository becomes a boundary when there is real persistence to replace or integrate.
Then I would add cross-cutting capabilities in the order the product needs them: validated configuration, consistent errors, contextual logging, authentication, and observability. I would not install queues, CQRS, or microservices in the initial template to demonstrate scalability.
Every convention should answer a concrete question: where is input validated, how is an error reported, who may execute this operation, and how is it tested? If the team cannot explain the convention during code review, the framework will not make it understandable automatically.
This path lets NestJS provide structure without turning its entire documentation into a requirements checklist. The application grows from observed needs, not every capability the framework offers.
Before distributing modules as services, see why a vertical-slice monolith is often the better starting point. The broader criterion is the same one I explore in perfect code vs correct decisions: adopt structure when it solves a present problem, not to anticipate every possible problem.



