Building Scalable Node.js APIs with NestJS: A Production-Ready Guide
Introduction
As Node.js applications grow in complexity, maintaining clean, scalable code becomes increasingly challenging. NestJS addresses this by bringing structure and enterprise-level patterns to Node.js development. In this guide, we'll explore how to build production-ready APIs using NestJS's powerful features.
Why Choose NestJS Over Express?
While Express offers flexibility, NestJS provides:
- Dependency Injection: Built-in IoC container for better testability
- Modular Architecture: Organized code structure that scales
- TypeScript First: Full TypeScript support out of the box
- Decorator-Based: Clean, readable code with decorators
- Built-in Features: Guards, interceptors, pipes, and middleware
Setting Up Your First NestJS Project
Let's start by creating a new NestJS project:
npm i -g @nestjs/cli
nest new my-api
cd my-api
npm run start:devBuilding a User Management API
Creating the User Module
NestJS organizes code into modules. Let's create a user module:
nest generate module users
nest generate controller users
nest generate service usersDefining the User Entity
First, let's define our user entity with proper validation:
// src/users/dto/create-user.dto.ts
import { IsEmail, IsNotEmpty, MinLength } from 'class-validator';
export class CreateUserDto {
@IsNotEmpty()
@MinLength(2)
name: string;
@IsEmail()
email: string;
@MinLength(8)
password: string;
}Implementing the User Service
The service layer handles business logic:
// src/users/users.service.ts
import { Injectable, ConflictException, NotFoundException } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import * as bcrypt from 'bcrypt';
@Injectable()
export class UsersService {
private users = [];
async create(createUserDto: CreateUserDto) {
const existingUser = this.users.find(u => u.email === createUserDto.email);
if (existingUser) {
throw new ConflictException('User with this email already exists');
}
const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
const user = {
id: Date.now(),
...createUserDto,
password: hashedPassword,
createdAt: new Date()
};
this.users.push(user);
const { password, ...result } = user;
return result;
}
findAll() {
return this.users.map(({ password, ...user }) => user);
}
findOne(id: number) {
const user = this.users.find(u => u.id === id);
if (!user) {
throw new NotFoundException('User not found');
}
const { password, ...result } = user;
return result;
}
}Building the Controller
Controllers handle HTTP requests and responses:
// src/users/users.controller.ts
import { Controller, Get, Post, Body, Param, ParseIntPipe, UseGuards } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
@Get()
findAll() {
return this.usersService.findAll();
}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.usersService.findOne(id);
}
}Advanced NestJS Features
Global Exception Handling
Create a global exception filter for consistent error responses:
// src/common/filters/http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const status = exception.getStatus();
response.status(status).json({
statusCode: status,
message: exception.message,
timestamp: new Date().toISOString()
});
}
} Request Validation Pipe
Enable global validation in your main.ts:
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true
}));
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
bootstrap();Database Integration with TypeORM
For production applications, integrate a real database:
npm install @nestjs/typeorm typeorm mysql2
// app.module.ts
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'password',
database: 'nestjs_db',
autoLoadEntities: true,
synchronize: process.env.NODE_ENV !== 'production'
}),
UsersModule
]
})
export class AppModule {}Testing Your NestJS Application
NestJS provides excellent testing utilities:
// users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
describe('UsersService', () => {
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UsersService]
}).compile();
service = module.get(UsersService);
});
it('should create a user', async () => {
const userData = { name: 'John', email: 'john@example.com', password: 'password123' };
const user = await service.create(userData);
expect(user.name).toBe('John');
expect(user.password).toBeUndefined();
});
}); Production Best Practices
- Environment Configuration: Use @nestjs/config for environment variables
- Logging: Implement structured logging with Winston
- Rate Limiting: Add @nestjs/throttler for API protection
- Swagger Documentation: Use @nestjs/swagger for API docs
- Health Checks: Implement @nestjs/terminus for monitoring
Conclusion
NestJS provides a robust foundation for building scalable Node.js applications. Its modular architecture, dependency injection, and TypeScript-first approach make it ideal for enterprise applications. The framework's extensive ecosystem and built-in features significantly reduce development time while maintaining code quality and testability.
Related Posts
Building Scalable GraphQL APIs with NestJS: A Practical Guide
Learn to create powerful, type-safe GraphQL APIs using NestJS with practical examples and best practices for scalable applications.
Building Production-Ready REST APIs with FastAPI and Pydantic
Learn how to build robust, type-safe REST APIs using FastAPI and Pydantic with proper validation, documentation, and error handling.
Implementing GraphQL with NestJS: A Complete Guide for Modern API Development
Learn how to build scalable GraphQL APIs with NestJS using decorators, resolvers, and type-safe schemas for modern backend development.