NestJS is the backend framework I use on every project that needs a robust API. It's Node.js with real architecture, not a 2000-line index.js file.
What makes the difference: dependency injection, modular architecture, and native TypeScript. Every service, every controller, every guard has its place. Code is testable, maintainable, and a new developer can understand the structure in minutes.
Express is an excellent starting point. But when a project grows, without imposed structure, the code becomes unmanageable. NestJS solves this from the first commit.
Express gives total freedom. That's its strength and its weakness. On a small project, it works. On a business app with 50 endpoints, complex business logic and 3 developers, the lack of conventions becomes a problem.
Modular architecture. NestJS organizes code into modules. Each module groups its controllers, services and providers. A users module, a billing module, a notifications module. Each is independent, testable, replaceable.
Dependency injection. Services are automatically injected where needed. No cascading require(), no manual singletons. The framework manages instance lifecycle.
TypeScript decorators. Routes, validations, guards, documentation are declared via decorators. @Get(), @Body(), @UseGuards(). Code is declarative and readable.
Validation pipes. Incoming data is automatically validated with class-validator. A malformed DTO returns a 400 error before even reaching the controller. No more scattered manual validations.
Modules by business domain. I split each application by domain, not by technical layer. An orders module contains its controller, service, DTOs and tests. No global controllers/ folder with 30 files.
Typed DTOs for each operation. CreateUserDto, UpdateUserDto, UserResponseDto. Each operation has its own DTO with built-in validation. The frontend knows exactly what it can send and what it will receive.
Guards for security. JWT auth via Passport, role guards (@Roles('admin')), custom guards for business logic. Security is declarative, not scattered across code.
Interceptors for cross-cutting concerns. Logging, response transformation, caching, timing. Interceptors encapsulate logic that applies to all endpoints without polluting controllers.
Unit and e2e tests. Each service is unit tested with injected mocks. Critical endpoints have e2e tests that verify the complete flow from HTTP request to response.
Here's how I structure a production NestJS API:
Auth module. JWT + refresh tokens, Passport strategies (local, Google OAuth), rate limiting middleware. Centralized session and permission management.
Database module. TypeORM or Prisma with PostgreSQL. Versioned migrations, development seeds, transactions for critical operations.
Queue module. BullMQ with Redis for async tasks: email sending, PDF generation, third-party data sync. Jobs are typed, retryable and monitorable.
Config module. Environment variables validated at startup with @nestjs/config + Joi. If a critical variable is missing, the app won't start rather than crashing in production.
Health module. /health endpoint checking database, Redis, third-party services. Used by monitoring and load balancers.