Complete Guide to NestJS: The Progressive Node.js Framework

Complete Guide to NestJS: The Progressive Node.js Framework

Introduction to NestJS

NestJS is a progressive Node.js framework for building efficient, reliable, and scalable server-side applications. It uses modern JavaScript (or TypeScript), combining elements of Object-Oriented Programming (OOP), Functional Programming (FP), and Functional Reactive Programming (FRP).

Created by Kamil Myśliwiec in 2017, NestJS has quickly gained popularity among developers for its modular architecture and familiar approach to building server-side applications, especially for those coming from Angular background.

Key Features:

  • Built with TypeScript (supports pure JavaScript)
  • Modular architecture inspired by Angular
  • Provides an out-of-the-box application architecture
  • Integrates with popular technologies like TypeORM, Mongoose, GraphQL, WebSockets, etc.
  • Uses Express.js (or Fastify) under the hood
  • Excellent for building microservices

Why Choose NestJS Over Other Node.js Frameworks?

While Express.js and other Node.js frameworks are great for building simple applications, NestJS provides a more structured approach that becomes increasingly valuable as your application grows in complexity.

Feature Express.js NestJS
Architecture Minimal, flexible Structured, modular
Scalability Requires manual organization Built-in modular system
TypeScript Support Possible but not built-in First-class citizen
Dependency Injection Not included Built-in
Testing Manual setup Integrated testing utilities
Learning Curve Low Moderate (easier for Angular developers)

NestJS is particularly well-suited for:

  • Enterprise-grade applications
  • Complex backends that need to scale
  • Teams that value maintainability and structure
  • Projects that might grow in complexity over time
  • Developers familiar with Angular's architecture

Core Concepts of NestJS

1. Modules

Modules are the fundamental building blocks of NestJS applications. Each NestJS application has at least one module (the root module), but well-structured applications have many feature modules.

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}

Modules help organize the application into cohesive blocks of functionality. They can import other modules and export providers to make them available to other modules.

2. Controllers

Controllers are responsible for handling incoming requests and returning responses to the client.

import { Controller, Get, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Post()
  create(@Body() createCatDto: CreateCatDto) {
    return this.catsService.create(createCatDto);
  }

  @Get()
  findAll() {
    return this.catsService.findAll();
  }
}

3. Providers (Services)

Providers are plain JavaScript classes that can be injected as dependencies. They handle business logic and data access.

import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  create(cat: Cat) {
    this.cats.push(cat);
  }

  findAll(): Cat[] {
    return this.cats;
  }
}

4. Dependency Injection

NestJS has a built-in dependency injection system that makes it easy to manage dependencies between different parts of your application.

constructor(private catsService: CatsService) {}

The framework will automatically create and inject instances of the required classes.

Setting Up a NestJS Project

Installation

First, install the Nest CLI globally:

npm install -g @nestjs/cli

Then create a new project:

nest new project-name

Project Structure

A new NestJS project has the following structure:

src/
├── app.controller.ts       # Basic controller with a single route
├── app.controller.spec.ts  # Unit tests for the controller
├── app.module.ts           # Root module of the application
├── app.service.ts          # Basic service
└── main.ts                 # Application entry file

Running the Application

To start the development server with hot-reload:

npm run start:dev

The application will be available at http://localhost:3000.

Building a REST API with NestJS

Let's build a complete CRUD API for a blog post system.

1. Generate Resources

nest generate module posts
nest generate controller posts
nest generate service posts

2. Create DTOs (Data Transfer Objects)

Create src/posts/dto/create-post.dto.ts:

export class CreatePostDto {
  readonly title: string;
  readonly content: string;
  readonly author: string;
}

Create src/posts/dto/update-post.dto.ts:

import { PartialType } from '@nestjs/mapped-types';
import { CreatePostDto } from './create-post.dto';

export class UpdatePostDto extends PartialType(CreatePostDto) {}

3. Implement the Service

Update src/posts/posts.service.ts:

import { Injectable } from '@nestjs/common';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';

@Injectable()
export class PostsService {
  private posts = [];
  private idCounter = 0;

  create(createPostDto: CreatePostDto) {
    const newPost = { id: ++this.idCounter, ...createPostDto };
    this.posts.push(newPost);
    return newPost;
  }

  findAll() {
    return this.posts;
  }

  findOne(id: number) {
    return this.posts.find(post => post.id === id);
  }

  update(id: number, updatePostDto: UpdatePostDto) {
    const postIndex = this.posts.findIndex(post => post.id === id);
    if (postIndex > -1) {
      this.posts[postIndex] = { ...this.posts[postIndex], ...updatePostDto };
      return this.posts[postIndex];
    }
    return null;
  }

  remove(id: number) {
    const postIndex = this.posts.findIndex(post => post.id === id);
    if (postIndex > -1) {
      return this.posts.splice(postIndex, 1)[0];
    }
    return null;
  }
}

4. Implement the Controller

Update src/posts/posts.controller.ts:

import { Controller, Get, Post, Body, Param, Put, Delete } from '@nestjs/common';
import { PostsService } from './posts.service';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';

@Controller('posts')
export class PostsController {
  constructor(private readonly postsService: PostsService) {}

  @Post()
  create(@Body() createPostDto: CreatePostDto) {
    return this.postsService.create(createPostDto);
  }

  @Get()
  findAll() {
    return this.postsService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.postsService.findOne(+id);
  }

  @Put(':id')
  update(@Param('id') id: string, @Body() updatePostDto: UpdatePostDto) {
    return this.postsService.update(+id, updatePostDto);
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.postsService.remove(+id);
  }
}

5. Update the Module

Update src/posts/posts.module.ts:

import { Module } from '@nestjs/common';
import { PostsController } from './posts.controller';
import { PostsService } from './posts.service';

@Module({
  controllers: [PostsController],
  providers: [PostsService],
})
export class PostsModule {}

6. Import the PostsModule in AppModule

Update src/app.module.ts:

import { Module } from '@nestjs/common';
import { PostsModule } from './posts/posts.module';

@Module({
  imports: [PostsModule],
})
export class AppModule {}

Advanced NestJS Features

1. Middleware

Middleware functions have access to the request and response objects, and the next middleware function in the application's request-response cycle.

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(Request...);
    next();
  }
}

Apply middleware in your module:

export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('*');
  }
}

2. Exception Filters

Nest comes with a built-in exceptions layer that handles all unhandled exceptions.

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();
    const request = ctx.getRequest();
    const status = exception.getStatus();

    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
      });
  }
}

Use it in your controller:

@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}

3. Pipes

Pipes transform input data to the desired form or validate data.

import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';

@Injectable()
export class ValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    if (!value) {
      throw new BadRequestException('No data submitted');
    }
    return value;
  }
}

Use it in your controller:

@Post()
@UsePipes(new ValidationPipe())
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

4. Guards

Guards determine whether a given request will be handled by the route handler or not.

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise | Observable {
    const request = context.switchToHttp().getRequest();
    return validateRequest(request);
  }
}

Use it in your controller:

@UseGuards(AuthGuard)
@Get('profile')
getProfile() {
  return this.userService.getProfile();
}

5. Interceptors

Interceptors can bind extra logic before or after method execution, transform the result returned from a function, or extend the basic function behavior.

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable {
    console.log('Before...');

    const now = Date.now();
    return next
      .handle()
      .pipe(
        tap(() => console.log(After... {Date.now() - now}ms)),
      );
  }
}

Use it in your controller:

@UseInterceptors(LoggingInterceptor)
@Get()
findAll() {
  return this.catsService.findAll();
}

Database Integration

NestJS works with many databases and ORMs. Let's look at integrating with TypeORM (for SQL databases) and Mongoose (for MongoDB).

1. TypeORM Integration

Install required packages:

npm install @nestjs/typeorm typeorm mysql

Set up the connection in your module:

import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      entities: [],
      synchronize: true,
    }),
  ],
})
export class AppModule {}

Create an entity:

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Post {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  title: string;

  @Column()
  content: string;

  @Column()
  author: string;
}

Create a repository:

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Post } from './post.entity';

@Injectable()
export class PostsService {
  constructor(
    @InjectRepository(Post)
    private postsRepository: Repository,
  ) {}

  findAll(): Promise {
    return this.postsRepository.find();
  }

  findOne(id: number): Promise {
    return this.postsRepository.findOne(id);
  }

  async remove(id: number): Promise {
    await this.postsRepository.delete(id);
  }
}

2. Mongoose Integration

Install required packages:

npm install @nestjs/mongoose mongoose

Set up the connection in your module:

import { MongooseModule } from '@nestjs/mongoose';

@Module({
  imports: [MongooseModule.forRoot('mongodb://localhost/nest')],
})
export class AppModule {}

Create a schema and model:

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';

@Schema()
export class Post extends Document {
  @Prop()
  title: string;

  @Prop()
  content: string;

  @Prop()
  author: string;
}

export const PostSchema = SchemaFactory.createForClass(Post);

Create a service:

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Post } from './schemas/post.schema';

@Injectable()
export class PostsService {
  constructor(@InjectModel(Post.name) private postModel: Model) {}

  async create(createPostDto: CreatePostDto): Promise {
    const createdPost = new this.postModel(createPostDto);
    return createdPost.save();
  }

  async findAll(): Promise {
    return this.postModel.find().exec();
  }
}

Authentication and Authorization

NestJS provides robust tools for implementing authentication and authorization. Let's implement JWT authentication.

1. Install Required Packages

npm install @nestjs/passport passport passport-jwt @nestjs/jwt bcrypt

2. Create Auth Module

nest generate module auth
nest generate service auth
nest generate controller auth

3. Implement User Service

import { Injectable } from '@nestjs/common';
import * as bcrypt from 'bcrypt';

@Injectable()
export class UsersService {
  private readonly users = [
    {
      userId: 1,
      username: 'john',
      password: '$2b$10$abcdefghijklmnopqrstuvwxyz123456',
    },
    {
      userId: 2,
      username: 'maria',
      password: '$2b$10$abcdefghijklmnopqrstuvwxyz123456',
    },
  ];

  async findOne(username: string): Promise {
    return this.users.find(user => user.username === username);
  }

  async validateUser(username: string, pass: string): Promise {
    const user = await this.findOne(username);
    if (user && await bcrypt.compare(pass, user.password)) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }
}

4. Implement JWT Strategy

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { jwtConstants } from './constants';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: jwtConstants.secret,
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username };
  }
}

5. Implement Auth Service

import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';

@Injectable()
export class AuthService {
  constructor(
    private usersService: UsersService,
    private jwtService: JwtService,
  ) {}

  async validateUser(username: string, pass: string): Promise {
    const user = await this.usersService.findOne(username);
    if (user && await bcrypt.compare(pass, user.password)) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }

  async login(user: any) {
    const payload = { username: user.username, sub: user.userId };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}

6. Implement Auth Controller

import { Controller, Post, Request, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';

@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService) {}

  @UseGuards(AuthGuard('local'))
  @Post('login')
  async login(@Request() req) {
    return this.authService.login(req.user);
  }
}

7. Protect Routes

@UseGuards(AuthGuard('jwt'))
@Get('profile')
getProfile(@Request() req) {
  return req.user;
}

Testing in NestJS

NestJS provides excellent testing utilities out of the box. Let's look at unit testing and e2e testing.

1. Unit Testing

Test a service:

import { Test, TestingModule } from '@nestjs/testing';
import { PostsService } from './posts.service';

describe('PostsService', () => {
  let service: PostsService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [PostsService],
    }).compile();

    service = module.get(PostsService);
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  it('should create a post', () => {
    const post = service.create({ title: 'Test', content: 'Content', author: 'Author' });
    expect(post.title).toBe('Test');
  });
});

2. E2E Testing

Test an entire application:

import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';

describe('AppController (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!');
  });
});

Deployment

To deploy a NestJS application:

1. Build the Application

npm run build

2. Run in Production

npm run start:prod

3. Using PM2 (Process Manager)

npm install pm2 -g
pm2 start dist/main.js

4. Docker Deployment

Create a Dockerfile:

FROM node:16

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install

COPY . .

RUN npm run build

EXPOSE 3000
CMD ["node", "dist/main.js"]

Build and run:

docker build -t nest-app .
docker run -p 3000:3000 nest-app

Conclusion

NestJS is a powerful framework that brings structure and scalability to Node.js applications. Its modular architecture, dependency injection system, and extensive ecosystem make it an excellent choice for building enterprise-grade applications.

Key takeaways:

  • NestJS provides a structured approach to building Node.js applications
  • The framework is heavily inspired by Angular, making it familiar to frontend developers
  • Modules, controllers, and providers are the core building blocks
  • NestJS integrates well with databases, authentication systems, and other technologies
  • The framework provides excellent testing utilities
  • NestJS applications can be deployed using various methods

Whether you're building a small API or a large-scale enterprise application, NestJS provides the tools and architecture you need to create maintainable, scalable, and efficient server-side applications.

Next Steps

To continue your NestJS journey:

  • Explore the official NestJS documentation
  • Learn about microservices architecture with NestJS
  • Implement GraphQL with NestJS
  • Explore WebSockets for real-time applications
  • Learn about advanced patterns like CQRS
  • Contribute to the open-source NestJS ecosystem