Introduction to Nest.js for Backend Development
Introduction: Why Choose Nest.js?
Hello everyone! I'm Shailesh Chaudhari, a full-stack developer with extensive experience in backend development. Today, I'll introduce you to Nest.js, a progressive Node.js framework that has revolutionized how we build scalable and maintainable backend applications.
Nest.js combines the best of object-oriented programming, functional programming, and functional reactive programming. It's built with TypeScript and takes inspiration from Angular's architecture, making it familiar to frontend developers while providing powerful backend capabilities.
Setting Up Your First Nest.js Project
Prerequisites
- Node.js (version 16 or higher)
- TypeScript knowledge (recommended)
- Basic understanding of REST APIs
- Familiarity with dependency injection concepts
Project Initialization
# Install Nest.js CLI globally
npm install -g @nestjs/cli
# Create a new Nest.js project
nest new my-nest-app
# Navigate to the project directory
cd my-nest-app
# Start the development server
npm run start:dev
The CLI will generate a well-structured project with the following layout:
src/
├── app.controller.ts
├── app.controller.spec.ts
├── app.module.ts
├── app.service.ts
└── main.ts
test/
├── app.e2e-spec.ts
├── jest-e2e.json
package.json
tsconfig.json
nest-cli.json
Understanding Nest.js Architecture
Modules: The Building Blocks
Modules are the fundamental building blocks of Nest.js applications. They help organize code into cohesive, reusable units:
// app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './users/users.module';
import { AuthModule } from './auth/auth.module';
@Module({
imports: [UsersModule, AuthModule], // Other modules
controllers: [AppController], // Controllers in this module
providers: [AppService], // Services/providers
exports: [AppService], // What this module exports
})
export class AppModule {}
Controllers: Handling HTTP Requests
Controllers define the routes and handle incoming HTTP requests:
// app.controller.ts
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { AppService } from './app.service';
@Controller('api')
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
@Get('users/:id')
getUserById(@Param('id') id: string): string {
return `User ID: ${id}`;
}
@Post('users')
createUser(@Body() userData: any): any {
return this.appService.createUser(userData);
}
}
Services: Business Logic Layer
Services contain the business logic and are injected into controllers:
// app.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World from Nest.js!';
}
createUser(userData: any): any {
// Business logic for creating a user
return {
id: Date.now(),
...userData,
createdAt: new Date(),
};
}
getUsers(): any[] {
// Simulate database query
return [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' },
];
}
}
Dependency Injection: The Core Concept
How Dependency Injection Works
Nest.js uses dependency injection to manage class dependencies automatically:
// user.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserService {
private users = [];
create(userData: any) {
const user = { id: Date.now(), ...userData };
this.users.push(user);
return user;
}
findAll() {
return this.users;
}
findOne(id: number) {
return this.users.find(user => user.id === id);
}
}
// user.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common';
import { UserService } from './user.service';
@Controller('users')
export class UserController {
// UserService is automatically injected
constructor(private readonly userService: UserService) {}
@Post()
create(@Body() userData: any) {
return this.userService.create(userData);
}
@Get()
findAll() {
return this.userService.findAll();
}
}
Custom Providers
You can create custom providers for complex dependencies:
// database.provider.ts
import { Provider } from '@nestjs/common';
export const databaseProvider: Provider = {
provide: 'DATABASE_CONNECTION',
useFactory: async () => {
// Database connection logic
return await createDatabaseConnection();
},
};
// app.module.ts
import { databaseProvider } from './database.provider';
@Module({
providers: [databaseProvider],
})
export class AppModule {}
Building a Complete CRUD API
Creating a User Management Module
// users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService], // Export for use in other modules
})
export class UsersModule {}
User Entity/Model
// users/user.entity.ts
export class User {
id: number;
name: string;
email: string;
age?: number;
createdAt: Date;
updatedAt: Date;
constructor(partial: Partial<User>) {
Object.assign(this, partial);
}
}
// Or with validation using class-validator
import { IsEmail, IsString, MinLength } from 'class-validator';
export class CreateUserDto {
@IsString()
@MinLength(2)
name: string;
@IsEmail()
email: string;
@IsString()
password: string;
}
export class UpdateUserDto {
@IsString()
@MinLength(2)
name?: string;
@IsEmail()
email?: string;
}
Complete CRUD Controller
// users/users.controller.ts
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Query,
HttpStatus,
HttpCode,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto, UpdateUserDto } from './dto/user.dto';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
@HttpCode(HttpStatus.CREATED)
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
@Get()
findAll(@Query('page') page = 1, @Query('limit') limit = 10) {
return this.usersService.findAll({ page: +page, limit: +limit });
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(+id);
}
@Put(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.usersService.update(+id, updateUserDto);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
remove(@Param('id') id: string) {
return this.usersService.remove(+id);
}
}
User Service with Business Logic
// users/users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { User } from './user.entity';
import { CreateUserDto, UpdateUserDto } from './dto/user.dto';
@Injectable()
export class UsersService {
private users: User[] = [];
private nextId = 1;
create(createUserDto: CreateUserDto): User {
const user = new User({
id: this.nextId++,
...createUserDto,
createdAt: new Date(),
updatedAt: new Date(),
});
this.users.push(user);
return user;
}
findAll(options: { page: number; limit: number } = { page: 1, limit: 10 }) {
const { page, limit } = options;
const startIndex = (page - 1) * limit;
const endIndex = startIndex + limit;
const paginatedUsers = this.users.slice(startIndex, endIndex);
return {
data: paginatedUsers,
meta: {
total: this.users.length,
page,
limit,
totalPages: Math.ceil(this.users.length / limit),
},
};
}
findOne(id: number): User {
const user = this.users.find(user => user.id === id);
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
}
update(id: number, updateUserDto: UpdateUserDto): User {
const userIndex = this.users.findIndex(user => user.id === id);
if (userIndex === -1) {
throw new NotFoundException(`User with ID ${id} not found`);
}
const updatedUser = {
...this.users[userIndex],
...updateUserDto,
updatedAt: new Date(),
};
this.users[userIndex] = updatedUser;
return updatedUser;
}
remove(id: number): void {
const userIndex = this.users.findIndex(user => user.id === id);
if (userIndex === -1) {
throw new NotFoundException(`User with ID ${id} not found`);
}
this.users.splice(userIndex, 1);
}
}
Advanced Nest.js Features
Middleware: Request Processing
// logger.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
}
}
// app.module.ts
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { LoggerMiddleware } from './logger.middleware';
@Module({})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggerMiddleware).forRoutes('*');
}
}
Guards: Authorization
// auth.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return this.validateRequest(request);
}
private validateRequest(request: any): boolean {
// Implement your authentication logic
const token = request.headers.authorization;
return token === 'valid-token'; // Simplified example
}
}
// Use the guard
@Controller('protected')
@UseGuards(AuthGuard)
export class ProtectedController {
@Get()
getProtectedData() {
return { message: 'This is protected data' };
}
}
Interceptors: Response Transformation
// transform.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, any> {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map(data => ({
success: true,
data,
timestamp: new Date().toISOString(),
})),
);
}
}
// app.module.ts
import { APP_INTERCEPTOR } from '@nestjs/core';
import { TransformInterceptor } from './transform.interceptor';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: TransformInterceptor,
},
],
})
export class AppModule {}
Pipes: Data Validation and Transformation
// validation.pipe.ts
import {
PipeTransform,
Injectable,
ArgumentMetadata,
BadRequestException,
} from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
async transform(value: any, { metatype }: ArgumentMetadata) {
if (!metatype || !this.toValidate(metatype)) {
return value;
}
const object = plainToClass(metatype, value);
const errors = await validate(object);
if (errors.length > 0) {
throw new BadRequestException('Validation failed');
}
return value;
}
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
}
// Use the pipe globally
// main.ts
import { ValidationPipe } from './validation.pipe';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
Database Integration
Using TypeORM with Nest.js
// app.module.ts
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './users/user.entity';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'user',
password: 'password',
database: 'nest_db',
entities: [User],
synchronize: true, // Don't use in production
}),
TypeOrmModule.forFeature([User]),
],
})
export class AppModule {}
Repository Pattern
// users/users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
findAll(): Promise<User[]> {
return this.usersRepository.find();
}
findOne(id: number): Promise<User | null> {
return this.usersRepository.findOneBy({ id });
}
async create(userData: Partial<User>): Promise<User> {
const user = this.usersRepository.create(userData);
return this.usersRepository.save(user);
}
async update(id: number, userData: Partial<User>): Promise<User> {
await this.usersRepository.update(id, userData);
return this.usersRepository.findOneBy({ id });
}
async remove(id: number): Promise<void> {
await this.usersRepository.delete(id);
}
}
Testing Nest.js Applications
Unit Testing
// 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>(UsersService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('should create a user', () => {
const userData = { name: 'Test User', email: 'test@example.com' };
const user = service.create(userData);
expect(user).toBeDefined();
expect(user.name).toBe(userData.name);
expect(user.email).toBe(userData.email);
});
});
E2E Testing
// app.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
describe('App (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
afterAll(async () => {
await app.close();
});
});
Deployment and Production Considerations
Environment Configuration
// config/configuration.ts
export default () => ({
port: parseInt(process.env.PORT, 10) || 3000,
database: {
host: process.env.DATABASE_HOST,
port: parseInt(process.env.DATABASE_PORT, 10) || 5432,
},
});
// app.module.ts
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [configuration],
}),
],
})
export class AppModule {}
Docker Integration
Process Management with PM2
Best Practices and Patterns
Project Structure for Large Applications
src/
├── modules/
│ ├── users/
│ │ ├── dto/
│ │ ├── entities/
│ │ ├── users.controller.ts
│ │ ├── users.service.ts
│ │ ├── users.module.ts
│ │ └── users.spec.ts
│ ├── auth/
│ └── products/
├── common/
│ ├── decorators/
│ ├── guards/
│ ├── interceptors/
│ ├── pipes/
│ └── filters/
├── config/
├── shared/
└── main.ts
Error Handling
// common/filters/http-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
} from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message: exception.message,
});
}
}
Logging
// Use built-in logger or integrate Winston
import { Logger } from '@nestjs/common';
@Injectable()
export class UsersService {
private readonly logger = new Logger(UsersService.name);
create(userData: any) {
this.logger.log(`Creating user: ${userData.email}`);
// ... implementation
}
}
Conclusion: Why Nest.js is Perfect for Enterprise Applications
Nest.js has established itself as one of the most powerful frameworks for building backend applications with Node.js. Its architecture, inspired by Angular, provides a solid foundation for developing scalable, maintainable, and testable applications.
The framework's emphasis on TypeScript, dependency injection, and modular architecture makes it particularly well-suited for enterprise applications where code quality, maintainability, and scalability are paramount.
Whether you're building REST APIs, GraphQL services, microservices, or real-time applications with WebSockets, Nest.js provides the tools and patterns you need to succeed. The rich ecosystem of modules and the active community ensure that you have access to solutions for almost any backend challenge.
As someone who has worked extensively with various backend frameworks, I can confidently recommend Nest.js for any serious backend development project. Its learning curve is worth the investment, and the productivity gains you'll experience make it an excellent choice for modern web development.
Start your Nest.js journey today, and discover why it's becoming the go-to framework for backend development in the Node.js ecosystem!
Happy coding! 🚀
"Nest.js doesn't just make backend development easier—it makes it better." - Shailesh Chaudhari