Why Node.js for Backend?
Node.js has established itself as a staple in backend development. The advantages are clear:
- One language everywhere: JavaScript/TypeScript in frontend and backend
- Large ecosystem: NPM is the world's largest package registry
- Performance: Non-blocking I/O makes Node.js ideal for I/O-intensive applications
- Developer productivity: Fast development cycles, large community
But Node.js alone is just a runtime. For structured applications, you need a framework.
Express vs. NestJS: The Comparison
Express: Minimalist and Flexible
Express is the most popular Node.js framework – and for good reason. It's simple, fast, and gives you maximum freedom:
const express = require('express');
const app = express();
app.get('/users', (req, res) => {
res.json([{ id: 1, name: 'Max' }]);
});
app.listen(3000);Express Advantages:
- Minimal overhead
- Quick start
- Full control over architecture
- Huge community and middleware ecosystem
Disadvantages:
- No predefined structure
- Architecture must be defined yourself
- TypeScript integration requires setup
- Growing projects quickly become messy
NestJS: Structure and Scalability
NestJS builds on Express (or Fastify) and brings a clear architecture:
// users.controller.ts
@Controller('users')
export class UsersController {
constructor(private usersService: UsersService) {}
@Get()
findAll(): User[] {
return this.usersService.findAll();
}
@Post()
create(@Body() createUserDto: CreateUserDto): User {
return this.usersService.create(createUserDto);
}
}NestJS Advantages:
- Clear architecture from the start (modules, controllers, services)
- TypeScript as first-class citizen
- Dependency injection out of the box
- Integrated validation, guards, interceptors
- Well documented with active community
Disadvantages:
- Steeper learning curve
- More boilerplate for small projects
- Opinionated – you work according to NestJS conventions
Understanding NestJS Architecture
NestJS follows the principle of modularity. Each feature is a module:
src/
├── app.module.ts # Root module
├── users/
│ ├── users.module.ts # Feature module
│ ├── users.controller.ts
│ ├── users.service.ts
│ └── dto/
│ └── create-user.dto.ts
├── auth/
│ ├── auth.module.ts
│ ├── auth.controller.ts
│ ├── auth.service.ts
│ └── guards/
│ └── jwt-auth.guard.tsModules
Modules group related functionality:
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService], // Available for other modules
})
export class UsersModule {}Controllers
Controllers handle HTTP requests:
@Controller('users')
export class UsersController {
@Get(':id')
findOne(@Param('id') id: string): Promise<User> {
return this.usersService.findOne(+id);
}
@Put(':id')
@UseGuards(JwtAuthGuard)
update(
@Param('id') id: string,
@Body() updateUserDto: UpdateUserDto,
): Promise<User> {
return this.usersService.update(+id, updateUserDto);
}
}Services
Services contain business logic:
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
async findAll(): Promise<User[]> {
return this.usersRepository.find();
}
async create(createUserDto: CreateUserDto): Promise<User> {
const user = this.usersRepository.create(createUserDto);
return this.usersRepository.save(user);
}
}Practical NestJS Features
1. Validation with DTOs
// create-user.dto.ts
export class CreateUserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(8)
password: string;
@IsOptional()
@IsString()
name?: string;
}2. Guards for Authentication
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
canActivate(context: ExecutionContext) {
return super.canActivate(context);
}
}
// Usage
@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@Request() req) {
return req.user;
}3. Interceptors for Logging/Transformation
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler) {
const now = Date.now();
return next.handle().pipe(
tap(() => console.log(`Request took ${Date.now() - now}ms`)),
);
}
}When to Use Which Framework?
Choose Express when:
- You're building a small project or prototype
- You need maximum flexibility
- The team already knows Express well
- You consciously want minimal overhead
Choose NestJS when:
- You're building a scalable, maintainable API
- You want to use TypeScript
- You're working with a team (consistency through conventions)
- You want features like validation, guards, Swagger docs out of the box
Our Experience
In our projects, we almost always use NestJS for new APIs. The initial extra effort pays off quickly: the codebase remains understandable even after months, new team members find their way quickly, and features like validation or authentication are implemented in minutes.
We still use Express for very small services or when integrating into an existing Express project.