Introduction to Nest.js for Backend Development

15 min read
Nest.js
Node.js
Backend Development
TypeScript
API Development
Microservices
Enterprise Applications
SC
Written by Shailesh Chaudhari
Full-Stack Developer & Problem Solver
TL;DR: Comprehensive introduction to Nest.js framework covering architecture, core concepts, modules, controllers, services, dependency injection, and best practices for building scalable backend applications with TypeScript.

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

SC
Written by Shailesh Chaudhari
Full-Stack Developer & Problem Solver