Toolmingo
Guides22 min read

NestJS Tutorial for Beginners (2025): Learn NestJS Step by Step

Complete NestJS tutorial for absolute beginners. Learn NestJS from scratch: modules, controllers, services, TypeORM, authentication, testing, and build real projects. Free guide with TypeScript code examples.

NestJS is the most popular TypeScript framework for building server-side Node.js applications. It combines the simplicity of Express with powerful architecture patterns from Angular — modules, dependency injection, decorators — making it the go-to choice for production-grade APIs, microservices, and enterprise backends.

This tutorial takes you from zero to building a complete REST API with authentication, database integration, and real-world patterns.

What you'll learn

Topic What you'll be able to do
Setup Scaffold and run your first NestJS app
Modules Organize code with NestJS module system
Controllers Handle HTTP requests with decorators
Services Write business logic with dependency injection
Pipes & Validation Validate and transform incoming data
TypeORM Connect to PostgreSQL and manage entities
Authentication Implement JWT auth with Passport
Guards & Interceptors Protect routes, transform responses
Testing Write unit and e2e tests
Real projects 2 complete production-ready projects

Why NestJS?

NestJS Express Fastify Hono Koa
TypeScript First-class Optional Optional First-class Optional
Architecture Opinionated (modules/DI) None None None Middleware-only
Learning curve Medium Low Low Low Low
Testability Excellent (built-in DI) Manual Manual Manual Manual
Ecosystem Large + Angular-inspired Massive Growing Growing Smaller
Best for Enterprise, microservices, scalable APIs Prototypes, simple APIs High-perf APIs Edge, tiny APIs Middleware-heavy
CLI Yes (nest CLI) No No No No

NestJS is the right choice when you need:

  • Structure at scale — the module system forces maintainable architecture
  • TypeScript everywhere — end-to-end type safety out of the box
  • Testability — dependency injection makes unit testing trivial
  • Ecosystem — built-in support for WebSockets, gRPC, GraphQL, microservices, queues

Prerequisites

Before starting, you should know:

  • JavaScript fundamentals (ES6+: classes, async/await, destructuring)
  • Basic TypeScript (types, interfaces, decorators)
  • Node.js basics (npm, package.json)
  • HTTP fundamentals (GET/POST/PUT/DELETE, status codes)

1. Setup: Your First NestJS App

Install the CLI

npm install -g @nestjs/cli

Create a new project

nest new my-api

The CLI prompts for a package manager (npm/yarn/pnpm). Choose npm. It generates:

my-api/
├── src/
│   ├── app.controller.ts      # Root controller
│   ├── app.controller.spec.ts # Controller tests
│   ├── app.module.ts          # Root module
│   ├── app.service.ts         # Root service
│   └── main.ts                # Entry point (bootstrap)
├── test/
│   ├── app.e2e-spec.ts
│   └── jest-e2e.json
├── nest-cli.json
├── package.json
└── tsconfig.json

Run the app

cd my-api
npm run start:dev    # Watch mode — restarts on file changes

Visit http://localhost:3000 — you'll see Hello World!.

main.ts — the bootstrap file

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

NestFactory.create() takes the root module and returns an application instance.


2. Core Building Blocks

NestJS has 4 core building blocks. Every application is composed of these:

Building block Decorator Purpose
Module @Module() Organizes related code into cohesive units
Controller @Controller() Handles incoming HTTP requests
Service @Injectable() Contains business logic, injected into controllers
Provider @Injectable() Any class that can be injected (services, repositories, factories)

3. Modules

Modules are the backbone of NestJS architecture. Every app has at least one module (AppModule). Large apps split into feature modules.

// app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],        // other modules this module depends on
  controllers: [AppController],  // controllers in this module
  providers: [AppService],       // services/providers in this module
  exports: [],        // what other modules can import from here
})
export class AppModule {}

Feature module example

nest generate module users      # or: nest g mo users
nest generate controller users
nest generate service users

This creates:

src/
└── users/
    ├── users.module.ts
    ├── users.controller.ts
    ├── users.controller.spec.ts
    ├── users.service.ts
    └── users.service.spec.ts
// 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 so other modules can use UsersService
})
export class UsersModule {}

Then import it in AppModule:

// app.module.ts
import { UsersModule } from './users/users.module';

@Module({
  imports: [UsersModule],
  // ...
})
export class AppModule {}

4. Controllers

Controllers handle HTTP requests. Each controller maps to a route prefix. Methods inside handle specific HTTP methods.

// users/users.controller.ts
import {
  Controller,
  Get,
  Post,
  Put,
  Delete,
  Body,
  Param,
  HttpCode,
  HttpStatus,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

@Controller('users')            // prefix: /users
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()                        // GET /users
  findAll() {
    return this.usersService.findAll();
  }

  @Get(':id')                   // GET /users/:id
  findOne(@Param('id') id: string) {
    return this.usersService.findOne(+id);
  }

  @Post()                       // POST /users
  @HttpCode(HttpStatus.CREATED)
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }

  @Put(':id')                   // PUT /users/:id
  update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
    return this.usersService.update(+id, updateUserDto);
  }

  @Delete(':id')                // DELETE /users/:id
  @HttpCode(HttpStatus.NO_CONTENT)
  remove(@Param('id') id: string) {
    return this.usersService.remove(+id);
  }
}

Common decorators

Decorator Purpose
@Get(), @Post(), @Put(), @Delete(), @Patch() HTTP method handlers
@Param('id') Route parameter (:id)
@Body() Request body
@Query('page') Query string parameter (?page=2)
@Headers('authorization') Request header
@Req() Raw Express/Fastify request object
@Res() Raw response object (avoid — breaks interceptors)
@HttpCode(201) Override default status code

5. Services

Services hold business logic. They are @Injectable() — NestJS's DI system handles instantiation.

// users/users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

export interface User {
  id: number;
  name: string;
  email: string;
}

@Injectable()
export class UsersService {
  private users: User[] = [];
  private idCounter = 1;

  findAll(): User[] {
    return this.users;
  }

  findOne(id: number): User {
    const user = this.users.find(u => u.id === id);
    if (!user) {
      throw new NotFoundException(`User #${id} not found`);
    }
    return user;
  }

  create(dto: CreateUserDto): User {
    const user: User = { id: this.idCounter++, ...dto };
    this.users.push(user);
    return user;
  }

  update(id: number, dto: UpdateUserDto): User {
    const user = this.findOne(id);
    Object.assign(user, dto);
    return user;
  }

  remove(id: number): void {
    const index = this.users.findIndex(u => u.id === id);
    if (index === -1) throw new NotFoundException(`User #${id} not found`);
    this.users.splice(index, 1);
  }
}

Dependency injection

NestJS uses constructor injection. The framework resolves dependencies automatically:

// If UsersController needs UsersService:
constructor(private readonly usersService: UsersService) {}

// If one service needs another:
// users.service.ts
constructor(private readonly emailService: EmailService) {}

The private readonly shorthand is idiomatic NestJS — it assigns the injected instance as a class property.


6. DTOs and Validation

Data Transfer Objects (DTOs) define the shape of request data. Combine them with class-validator for automatic validation.

npm install class-validator class-transformer
// users/dto/create-user.dto.ts
import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @MinLength(2)
  name: string;

  @IsEmail()
  email: string;

  @IsString()
  @MinLength(8)
  password: string;

  @IsOptional()
  @IsString()
  role?: string;
}

Enable the global ValidationPipe in main.ts:

import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,        // strip unknown properties
      forbidNonWhitelisted: true,  // throw if unknown properties sent
      transform: true,        // auto-transform types (string '1' → number 1)
    }),
  );
  await app.listen(3000);
}

Now if a client sends invalid data, NestJS automatically returns:

{
  "statusCode": 400,
  "message": ["email must be an email", "name must be longer than or equal to 2 characters"],
  "error": "Bad Request"
}

Common class-validator decorators

Decorator What it validates
@IsString() Must be a string
@IsNumber() Must be a number
@IsEmail() Valid email format
@IsUrl() Valid URL
@IsBoolean() Must be boolean
@IsArray() Must be an array
@IsEnum(MyEnum) Must be enum value
@MinLength(n) String min length
@MaxLength(n) String max length
@Min(n) / @Max(n) Number min/max
@IsOptional() Field is not required
@IsNotEmpty() Must not be empty string/array
@ValidateNested() Validate nested objects

7. Exception Handling

NestJS has built-in HTTP exceptions. Throw them from services or controllers:

import {
  NotFoundException,
  BadRequestException,
  UnauthorizedException,
  ForbiddenException,
  ConflictException,
  InternalServerErrorException,
} from '@nestjs/common';

// Throws 404
throw new NotFoundException('User not found');

// Throws 400
throw new BadRequestException('Invalid email format');

// Throws 401
throw new UnauthorizedException('Invalid credentials');

// Throws 403
throw new ForbiddenException('Admin access required');

// Throws 409
throw new ConflictException('Email already exists');

Custom exception filter

// 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();
    const exceptionResponse = exception.getResponse();

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message:
        typeof exceptionResponse === 'object'
          ? (exceptionResponse as any).message
          : exceptionResponse,
    });
  }
}

Apply globally:

// main.ts
app.useGlobalFilters(new HttpExceptionFilter());

8. Pipes

Pipes transform or validate data before it reaches a handler.

// Built-in pipes
import { ParseIntPipe, ParseUUIDPipe, DefaultValuePipe } from '@nestjs/common';

@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
  // 'id' is guaranteed to be a number, not a string
  return this.usersService.findOne(id);
}

@Get()
findAll(@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number) {
  return this.usersService.findAll(page);
}

Custom pipe

// common/pipes/trim.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';

@Injectable()
export class TrimPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    if (typeof value === 'string') {
      return value.trim();
    }
    if (typeof value === 'object' && value !== null) {
      return Object.fromEntries(
        Object.entries(value).map(([k, v]) => [
          k,
          typeof v === 'string' ? v.trim() : v,
        ]),
      );
    }
    return value;
  }
}

9. Guards

Guards decide whether a request should proceed. Used for authentication and authorization.

// common/guards/auth.guard.ts
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private jwtService: JwtService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest<Request>();
    const token = this.extractTokenFromHeader(request);
    if (!token) throw new UnauthorizedException();

    try {
      const payload = await this.jwtService.verifyAsync(token, {
        secret: process.env.JWT_SECRET,
      });
      request['user'] = payload;
    } catch {
      throw new UnauthorizedException();
    }
    return true;
  }

  private extractTokenFromHeader(request: Request): string | undefined {
    const [type, token] = request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : undefined;
  }
}

Apply a guard:

// Apply to single route
@UseGuards(AuthGuard)
@Get('profile')
getProfile(@Request() req) {
  return req.user;
}

// Apply to entire controller
@UseGuards(AuthGuard)
@Controller('users')
export class UsersController {}

// Apply globally
app.useGlobalGuards(new AuthGuard(jwtService));

Role-based access with custom decorator

// common/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

// common/guards/roles.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
      context.getHandler(),
      context.getClass(),
    ]);
    if (!requiredRoles) return true;

    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.some(role => user.roles?.includes(role));
  }
}

// Usage in controller
@Roles('admin')
@UseGuards(AuthGuard, RolesGuard)
@Delete(':id')
remove(@Param('id', ParseIntPipe) id: number) {
  return this.usersService.remove(id);
}

10. Interceptors

Interceptors wrap request/response logic. Common uses: logging, response transformation, caching, timeout.

// common/interceptors/transform.interceptor.ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

export interface Response<T> {
  data: T;
  timestamp: string;
}

@Injectable()
export class TransformInterceptor<T>
  implements NestInterceptor<T, Response<T>> {
  intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Observable<Response<T>> {
    return next.handle().pipe(
      map(data => ({
        data,
        timestamp: new Date().toISOString(),
      })),
    );
  }
}
// Apply globally in main.ts
app.useGlobalInterceptors(new TransformInterceptor());

// Now all responses become:
// { "data": { ... }, "timestamp": "2025-01-15T10:00:00.000Z" }

11. Database with TypeORM

TypeORM is the most common ORM choice for NestJS.

npm install @nestjs/typeorm typeorm pg

Connect to PostgreSQL

// app.module.ts
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: process.env.DB_HOST || 'localhost',
      port: parseInt(process.env.DB_PORT) || 5432,
      username: process.env.DB_USER || 'postgres',
      password: process.env.DB_PASSWORD || 'password',
      database: process.env.DB_NAME || 'myapp',
      autoLoadEntities: true,   // auto-detect entities registered with forFeature()
      synchronize: true,        // auto-create tables in dev (NEVER in production)
    }),
    UsersModule,
  ],
})
export class AppModule {}

Define an entity

// users/entities/user.entity.ts
import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  CreateDateColumn,
  UpdateDateColumn,
  BeforeInsert,
} from 'typeorm';
import * as bcrypt from 'bcrypt';

@Entity('users')
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ length: 100 })
  name: string;

  @Column({ unique: true })
  email: string;

  @Column({ select: false })  // never returned in queries by default
  password: string;

  @Column({ default: 'user' })
  role: string;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;

  @BeforeInsert()
  async hashPassword() {
    this.password = await bcrypt.hash(this.password, 10);
  }
}

Register and inject repository

// users/users.module.ts
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],  // register entity
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}
// users/users.service.ts
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  findAll(): Promise<User[]> {
    return this.usersRepository.find();
  }

  async findOne(id: number): Promise<User> {
    const user = await this.usersRepository.findOneBy({ id });
    if (!user) throw new NotFoundException(`User #${id} not found`);
    return user;
  }

  async findByEmail(email: string): Promise<User | null> {
    return this.usersRepository
      .createQueryBuilder('user')
      .addSelect('user.password')  // bypass select: false
      .where('user.email = :email', { email })
      .getOne();
  }

  async create(dto: CreateUserDto): Promise<User> {
    const existing = await this.usersRepository.findOneBy({ email: dto.email });
    if (existing) throw new ConflictException('Email already exists');
    const user = this.usersRepository.create(dto);
    return this.usersRepository.save(user);
  }

  async update(id: number, dto: UpdateUserDto): Promise<User> {
    const user = await this.findOne(id);
    Object.assign(user, dto);
    return this.usersRepository.save(user);
  }

  async remove(id: number): Promise<void> {
    const user = await this.findOne(id);
    await this.usersRepository.remove(user);
  }
}

TypeORM column types

Decorator Description
@PrimaryGeneratedColumn() Auto-increment integer PK
@PrimaryGeneratedColumn('uuid') UUID PK
@Column() Regular column (infers type from TS)
@Column({ type: 'text' }) Explicit type
@Column({ nullable: true }) Optional column
@Column({ unique: true }) Unique constraint
@Column({ default: 'user' }) Default value
@CreateDateColumn() Auto-set on insert
@UpdateDateColumn() Auto-set on update
@DeleteDateColumn() Soft delete timestamp

Relations

// one user has many posts
// user.entity.ts
@OneToMany(() => Post, post => post.author)
posts: Post[];

// post.entity.ts
@ManyToOne(() => User, user => user.posts, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'author_id' })
author: User;

// many-to-many (users ↔ roles)
@ManyToMany(() => Role)
@JoinTable()
roles: Role[];

// Loading relations in queries
const user = await this.usersRepository.findOne({
  where: { id },
  relations: { posts: true },  // eager load
});

// Or with QueryBuilder
const user = await this.usersRepository
  .createQueryBuilder('user')
  .leftJoinAndSelect('user.posts', 'post')
  .where('user.id = :id', { id })
  .getOne();

12. Authentication with JWT

npm install @nestjs/passport @nestjs/jwt passport passport-jwt passport-local bcrypt
npm install -D @types/passport-jwt @types/passport-local @types/bcrypt

Auth module structure

nest g module auth
nest g controller auth
nest g service auth
// auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtStrategy } from './strategies/jwt.strategy';
import { UsersModule } from '../users/users.module';

@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.register({
      secret: process.env.JWT_SECRET || 'super-secret',
      signOptions: { expiresIn: '7d' },
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService, JwtStrategy],
  exports: [AuthService, JwtModule],
})
export class AuthModule {}
// auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
import * as bcrypt from 'bcrypt';

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

  async signIn(email: string, password: string) {
    const user = await this.usersService.findByEmail(email);
    if (!user) throw new UnauthorizedException('Invalid credentials');

    const isMatch = await bcrypt.compare(password, user.password);
    if (!isMatch) throw new UnauthorizedException('Invalid credentials');

    const payload = { sub: user.id, email: user.email, role: user.role };
    return {
      access_token: await this.jwtService.signAsync(payload),
      user: { id: user.id, name: user.name, email: user.email },
    };
  }

  async register(dto: CreateUserDto) {
    return this.usersService.create(dto);
  }
}
// auth/strategies/jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: process.env.JWT_SECRET || 'super-secret',
    });
  }

  async validate(payload: any) {
    // returned value is attached to request.user
    return { userId: payload.sub, email: payload.email, role: payload.role };
  }
}
// auth/auth.controller.ts
import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
import { AuthService } from './auth.service';

export class SignInDto {
  email: string;
  password: string;
}

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

  @Post('register')
  register(@Body() dto: CreateUserDto) {
    return this.authService.register(dto);
  }

  @Post('login')
  @HttpCode(HttpStatus.OK)
  signIn(@Body() dto: SignInDto) {
    return this.authService.signIn(dto.email, dto.password);
  }
}

Use @UseGuards(AuthGuard('jwt')) or create a custom JwtAuthGuard:

// common/guards/jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

// Usage
@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@Request() req) {
  return req.user;  // { userId, email, role }
}

13. Configuration

Never hardcode secrets. Use @nestjs/config:

npm install @nestjs/config
// app.module.ts
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,   // available in all modules without re-importing
      envFilePath: '.env',
    }),
    // ...
  ],
})
export class AppModule {}
# .env
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USER=postgres
DATABASE_PASSWORD=secret
DATABASE_NAME=myapp
JWT_SECRET=your-very-long-random-secret
PORT=3000
// Inject ConfigService anywhere
import { ConfigService } from '@nestjs/config';

@Injectable()
export class SomeService {
  constructor(private configService: ConfigService) {}

  doSomething() {
    const dbHost = this.configService.get<string>('DATABASE_HOST');
    const port = this.configService.get<number>('PORT', 3000); // with default
  }
}

Typed config with validation

npm install joi
// 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,
    user: process.env.DATABASE_USER,
    password: process.env.DATABASE_PASSWORD,
    name: process.env.DATABASE_NAME,
  },
  jwt: {
    secret: process.env.JWT_SECRET,
    expiresIn: '7d',
  },
});

// Use in TypeOrmModule
TypeOrmModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    type: 'postgres',
    host: config.get('database.host'),
    port: config.get<number>('database.port'),
    username: config.get('database.user'),
    password: config.get('database.password'),
    database: config.get('database.name'),
    autoLoadEntities: true,
    synchronize: config.get('NODE_ENV') !== 'production',
  }),
}),

14. Testing

NestJS has excellent testing support via Jest.

Unit tests

// users/users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { Repository } from 'typeorm';
import { NotFoundException } from '@nestjs/common';

describe('UsersService', () => {
  let service: UsersService;
  let repository: jest.Mocked<Repository<User>>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UsersService,
        {
          provide: getRepositoryToken(User),
          useValue: {
            find: jest.fn(),
            findOneBy: jest.fn(),
            create: jest.fn(),
            save: jest.fn(),
            remove: jest.fn(),
          },
        },
      ],
    }).compile();

    service = module.get<UsersService>(UsersService);
    repository = module.get(getRepositoryToken(User));
  });

  describe('findOne', () => {
    it('returns user when found', async () => {
      const mockUser = { id: 1, name: 'Alice', email: 'alice@example.com' } as User;
      repository.findOneBy.mockResolvedValue(mockUser);

      const result = await service.findOne(1);
      expect(result).toEqual(mockUser);
      expect(repository.findOneBy).toHaveBeenCalledWith({ id: 1 });
    });

    it('throws NotFoundException when not found', async () => {
      repository.findOneBy.mockResolvedValue(null);

      await expect(service.findOne(999)).rejects.toThrow(NotFoundException);
    });
  });

  describe('create', () => {
    it('creates and saves a user', async () => {
      const dto = { name: 'Bob', email: 'bob@example.com', password: 'secret123' };
      const mockUser = { id: 2, ...dto } as User;

      repository.findOneBy.mockResolvedValue(null);  // no duplicate
      repository.create.mockReturnValue(mockUser);
      repository.save.mockResolvedValue(mockUser);

      const result = await service.create(dto);
      expect(result).toEqual(mockUser);
    });
  });
});

E2E tests

// test/users.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';

describe('Users (e2e)', () => {
  let app: INestApplication;
  let jwtToken: string;

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],  // use real app with test database
    }).compile();

    app = moduleFixture.createNestApplication();
    app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
    await app.init();
  });

  afterAll(() => app.close());

  it('POST /auth/register — creates a user', async () => {
    const response = await request(app.getHttpServer())
      .post('/auth/register')
      .send({ name: 'Alice', email: 'alice@test.com', password: 'password123' })
      .expect(201);

    expect(response.body).toHaveProperty('id');
    expect(response.body.email).toBe('alice@test.com');
  });

  it('POST /auth/login — returns JWT', async () => {
    const response = await request(app.getHttpServer())
      .post('/auth/login')
      .send({ email: 'alice@test.com', password: 'password123' })
      .expect(200);

    expect(response.body).toHaveProperty('access_token');
    jwtToken = response.body.access_token;
  });

  it('GET /users — requires auth', async () => {
    await request(app.getHttpServer()).get('/users').expect(401);
  });

  it('GET /users — returns users with JWT', async () => {
    const response = await request(app.getHttpServer())
      .get('/users')
      .set('Authorization', `Bearer ${jwtToken}`)
      .expect(200);

    expect(Array.isArray(response.body)).toBe(true);
  });
});

15. Project 1: Blog API

Let's build a complete blog REST API.

Entities

// posts/entities/post.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, CreateDateColumn, UpdateDateColumn } from 'typeorm';
import { User } from '../../users/entities/user.entity';

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

  @Column()
  title: string;

  @Column({ type: 'text' })
  content: string;

  @Column({ default: false })
  published: boolean;

  @ManyToOne(() => User, { onDelete: 'CASCADE' })
  author: User;

  @Column({ nullable: true })
  authorId: number;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

DTOs

// posts/dto/create-post.dto.ts
import { IsString, IsBoolean, IsOptional, MinLength } from 'class-validator';

export class CreatePostDto {
  @IsString()
  @MinLength(5)
  title: string;

  @IsString()
  @MinLength(20)
  content: string;

  @IsOptional()
  @IsBoolean()
  published?: boolean = false;
}

Service with pagination

// posts/posts.service.ts
@Injectable()
export class PostsService {
  constructor(
    @InjectRepository(Post) private postsRepo: Repository<Post>,
  ) {}

  async findAll(page = 1, limit = 10) {
    const [posts, total] = await this.postsRepo.findAndCount({
      where: { published: true },
      relations: { author: true },
      select: { author: { id: true, name: true } },
      order: { createdAt: 'DESC' },
      skip: (page - 1) * limit,
      take: limit,
    });
    return { posts, total, page, lastPage: Math.ceil(total / limit) };
  }

  async create(dto: CreatePostDto, userId: number): Promise<Post> {
    const post = this.postsRepo.create({ ...dto, authorId: userId });
    return this.postsRepo.save(post);
  }

  async findOwn(userId: number): Promise<Post[]> {
    return this.postsRepo.find({ where: { authorId: userId }, order: { createdAt: 'DESC' } });
  }
}

Controller

// posts/posts.controller.ts
@UseGuards(JwtAuthGuard)
@Controller('posts')
export class PostsController {
  constructor(private postsService: PostsService) {}

  @Get()
  findAll(
    @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
    @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
  ) {
    return this.postsService.findAll(page, limit);
  }

  @Post()
  create(@Body() dto: CreatePostDto, @Request() req) {
    return this.postsService.create(dto, req.user.userId);
  }

  @Get('mine')
  findOwn(@Request() req) {
    return this.postsService.findOwn(req.user.userId);
  }
}

16. Project 2: Task Manager API

// tasks/entities/task.entity.ts
export enum TaskStatus {
  TODO = 'TODO',
  IN_PROGRESS = 'IN_PROGRESS',
  DONE = 'DONE',
}

@Entity('tasks')
export class Task {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  title: string;

  @Column({ type: 'text', nullable: true })
  description: string;

  @Column({ type: 'enum', enum: TaskStatus, default: TaskStatus.TODO })
  status: TaskStatus;

  @Column({ type: 'date', nullable: true })
  dueDate: Date;

  @ManyToOne(() => User, { onDelete: 'CASCADE' })
  owner: User;

  @Column()
  ownerId: number;

  @CreateDateColumn()
  createdAt: Date;
}
// tasks/dto/create-task.dto.ts
import { IsString, IsOptional, IsEnum, IsDateString } from 'class-validator';

export class CreateTaskDto {
  @IsString()
  title: string;

  @IsOptional()
  @IsString()
  description?: string;

  @IsOptional()
  @IsDateString()
  dueDate?: string;
}

export class UpdateTaskStatusDto {
  @IsEnum(TaskStatus)
  status: TaskStatus;
}
// tasks/tasks.service.ts
@Injectable()
export class TasksService {
  constructor(@InjectRepository(Task) private tasksRepo: Repository<Task>) {}

  findByOwner(ownerId: number): Promise<Task[]> {
    return this.tasksRepo.find({
      where: { ownerId },
      order: { createdAt: 'DESC' },
    });
  }

  async create(dto: CreateTaskDto, ownerId: number): Promise<Task> {
    const task = this.tasksRepo.create({ ...dto, ownerId });
    return this.tasksRepo.save(task);
  }

  async updateStatus(id: string, status: TaskStatus, ownerId: number): Promise<Task> {
    const task = await this.tasksRepo.findOne({ where: { id, ownerId } });
    if (!task) throw new NotFoundException('Task not found');
    task.status = status;
    return this.tasksRepo.save(task);
  }

  async remove(id: string, ownerId: number): Promise<void> {
    const task = await this.tasksRepo.findOne({ where: { id, ownerId } });
    if (!task) throw new NotFoundException('Task not found');
    await this.tasksRepo.remove(task);
  }
}

Common Mistakes

Mistake Problem Fix
synchronize: true in production Drops/alters tables on deploy Set false, use migrations instead
Circular dependency between modules Runtime DI error Use forwardRef(() => Module)
Not using transform: true in ValidationPipe Query params stay as strings Enable transform: true
Forgetting @Module exports Service not injectable in other modules Export the service in its module
Using @Res() directly Bypasses interceptors and exception filters Return data from handler; only use @Res() for streaming
No global prefix on API routes Conflicts with frontend SPA routes app.setGlobalPrefix('api')
Returning passwords in responses Security vulnerability Add @Exclude() decorator + ClassSerializerInterceptor
Not validating env vars at startup Silent failures in production Use Joi schema validation in ConfigModule

NestJS vs Express vs Fastify vs Hono vs Koa

NestJS Express Fastify Hono Koa
TypeScript First-class Manual setup Optional First-class Optional
Architecture Opinionated (modules/DI) DIY DIY DIY DIY
Performance Fast Medium Fastest Very fast Fast
Learning curve Medium-High Low Low Low Medium
Dependency injection Built-in Manual Manual Manual Manual
CLI scaffolding Yes No No No No
WebSockets Built-in socket.io Manual Manual Manual
Microservices Built-in transports Manual Manual Manual Manual
GraphQL First-class integration Manual Manual No No
Enterprise adoption Very high Very high Growing Growing Low
Bundle size Large Small Small Tiny Small
Best for Enterprise APIs, microservices, monorepos Prototypes, simple APIs High-throughput APIs Edge/serverless, tiny Middleware-heavy apps

NestJS vs Related Concepts

Term What it means in NestJS context
Module Unit of organization; imports/exports providers
Provider Any @Injectable() class registered in a module
Guard Authorization check — runs before the handler
Interceptor Wraps request/response — for logging, transformation
Pipe Validates/transforms request data before handler
Filter Catches exceptions and formats error responses
Middleware Express-style function; runs before guards
Decorator TypeScript metadata — @Controller, @Get, etc.

Frequently Asked Questions

What is NestJS used for? NestJS builds server-side Node.js applications: REST APIs, GraphQL APIs, WebSocket servers, gRPC services, and microservices. It's popular in enterprise settings where code organization and testability matter at scale.

Is NestJS better than Express? For small projects and prototypes, Express is faster to start. For larger teams and codebases, NestJS's enforced architecture (modules, DI) prevents the spaghetti code that unstructured Express apps often become. NestJS uses Express (or Fastify) under the hood.

Do I need to know Angular to learn NestJS? No. NestJS is inspired by Angular's module/DI patterns, but you don't need Angular experience. If you know TypeScript and Node.js basics, NestJS is approachable.

What database should I use with NestJS? TypeORM (PostgreSQL, MySQL, SQLite) and Prisma are the most popular choices. Mongoose is used for MongoDB. NestJS has first-party integrations for all of them.

How do I handle file uploads in NestJS? Use @nestjs/platform-express with multer:

@Post('upload')
@UseInterceptors(FileInterceptor('file'))
uploadFile(@UploadedFile() file: Express.Multer.File) {
  return { filename: file.filename };
}

Is NestJS good for microservices? Yes — it's one of NestJS's strengths. It has built-in microservice transports (TCP, Redis, RabbitMQ, Kafka, gRPC, NATS) and a unified programming model for both HTTP and message-based services.

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools