All topics
Backend · Learning hub

NestJS notes for developers

Master NestJS with a curated set of 4 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore Backend notes
NestJS

Modules, Controllers & Services

Modules, Controllers & Services NestJS Architecture NestJS organizes code into Modules. Each module groups related Controllers (handle HTTP) and Providers/Servi

Modules, Controllers & Services

NestJS Architecture

NestJS organizes code into Modules. Each module groups related Controllers (handle HTTP) and Providers/Services (business logic). The root AppModule bootstraps the app.

// app.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';
import { AuthModule } from './auth/auth.module';

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

// users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  imports: [],
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService],  // make available to other modules
})
export class UsersModule {}

Controllers

import {
  Controller, Get, Post, Put, Patch, Delete,
  Body, Param, Query, Req, Res, HttpCode, HttpStatus,
  UseGuards, UseInterceptors, ParseIntPipe, ParseUUIDPipe,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';

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

  @Get()                    // GET /users
  findAll(@Query('page') page = 1, @Query('limit') limit = 10) {
    return this.usersService.findAll({ page, limit });
  }

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

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

  @Patch(':id')            // PATCH /users/:id
  update(@Param('id') id: string, @Body() updateDto: Partial<CreateUserDto>) {
    return this.usersService.update(id, updateDto);
  }

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

Services (Providers)

import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { CreateUserDto } from './dto/create-user.dto';

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

  async findAll(pagination: { page: number; limit: number }) {
    const { page, limit } = pagination;
    return this.usersRepo.findAndCount({
      skip: (page - 1) * limit,
      take: limit,
      order: { createdAt: 'DESC' },
    });
  }

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

  async create(dto: CreateUserDto): Promise<User> {
    const user = this.usersRepo.create(dto);
    return this.usersRepo.save(user);
  }

  async update(id: string, dto: Partial<CreateUserDto>): Promise<User> {
    await this.findOne(id);  // throws if not found
    await this.usersRepo.update(id, dto);
    return this.findOne(id);
  }

  async remove(id: string): Promise<void> {
    await this.findOne(id);
    await this.usersRepo.delete(id);
  }
}

DTOs & Validation

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

export class CreateUserDto {
  @IsEmail()
  email: string;

  @IsString()
  @MinLength(2)
  name: string;

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

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

// main.ts — enable global validation pipe
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,       // strip unknown properties
      forbidNonWhitelisted: true,
      transform: true,       // auto-convert types
    }),
  );
  await app.listen(3000);
}
bootstrap();

Exception Filters

// Built-in HTTP exceptions
import {
  BadRequestException,      // 400
  UnauthorizedException,    // 401
  ForbiddenException,       // 403
  NotFoundException,        // 404
  ConflictException,        // 409
  InternalServerErrorException, // 500
} from '@nestjs/common';

throw new NotFoundException('User not found');
throw new ConflictException({ message: 'Email already exists', field: 'email' });

// Custom exception filter
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const status = exception.getStatus();
    const exceptionResponse = exception.getResponse();

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      message: typeof exceptionResponse === 'object'
        ? exceptionResponse
        : { message: exceptionResponse },
    });
  }
}
NestJS

DI, Guards & Interceptors

DI, Guards & Interceptors Dependency Injection // Standard injection via constructor @Injectable() export class OrdersService { constructor( private readonly us

DI, Guards & Interceptors

Dependency Injection

// Standard injection via constructor
@Injectable()
export class OrdersService {
  constructor(
    private readonly usersService: UsersService,
    private readonly emailService: EmailService,
    private readonly configService: ConfigService,
  ) {}
}

// Custom provider — factory
@Module({
  providers: [
    {
      provide: 'DATABASE_CONNECTION',
      useFactory: async (config: ConfigService) => {
        return createConnection(config.get('DATABASE_URL'));
      },
      inject: [ConfigService],
    },
    {
      provide: 'APP_CONFIG',
      useValue: { timeout: 5000, retries: 3 },
    },
  ],
})
export class AppModule {}

// Inject custom provider
@Injectable()
export class AppService {
  constructor(
    @Inject('DATABASE_CONNECTION') private readonly db: Connection,
    @Inject('APP_CONFIG') private readonly config: { timeout: number },
  ) {}
}

// ConfigModule (built-in, global)
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env' }),
  ],
})
export class AppModule {}

// Use anywhere
constructor(private configService: ConfigService) {}
const dbUrl = this.configService.get<string>('DATABASE_URL');

Guards

import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';

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

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

    try {
      const payload = await this.jwtService.verifyAsync(token);
      request['user'] = payload;  // attach to request
      return true;
    } catch {
      throw new UnauthorizedException();
    }
  }

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

// Role-based guard using custom decorator
import { Reflector } from '@nestjs/core';

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

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!requiredRoles) return true;
    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.some(role => user.roles?.includes(role));
  }
}

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

// Applying guards
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
@Get('admin-only')
adminRoute() { ... }

// Global guard
app.useGlobalGuards(new JwtAuthGuard(jwtService));

Interceptors

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

// Transform response — wrap in { data: ... }
@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(map(data => ({ data, success: true })));
  }
}

// Logging interceptor
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const req = context.switchToHttp().getRequest();
    const start = Date.now();
    console.log(`--> ${req.method} ${req.url}`);
    return next.handle().pipe(
      tap(() => console.log(`<-- ${req.method} ${req.url} ${Date.now() - start}ms`)),
    );
  }
}

// Apply
@UseInterceptors(TransformInterceptor)
@Get()
findAll() { ... }

// Global
app.useGlobalInterceptors(new TransformInterceptor());

Pipes & Custom Decorators

// Built-in pipes
@Param('id', ParseIntPipe) id: number
@Param('id', ParseUUIDPipe) id: string
@Body(new ValidationPipe()) dto: CreateUserDto

// Custom pipe
@Injectable()
export class TrimPipe implements PipeTransform {
  transform(value: any) {
    if (typeof value === 'string') return value.trim();
    return value;
  }
}

// Custom param decorator
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const CurrentUser = createParamDecorator(
  (data: string | undefined, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return data ? request.user?.[data] : request.user;
  },
);

// Usage
@Get('profile')
getProfile(@CurrentUser() user: UserPayload) {
  return user;
}
NestJS

Database & Auth

Database & Auth TypeORM Setup // app.module.ts import { TypeOrmModule } from '@nestjs/typeorm'; @Module({ imports: [ TypeOrmModule.forRootAsync({ imports: [Conf

Database & Auth

TypeORM Setup

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

@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: (config: ConfigService) => ({
        type: 'postgres',
        url: config.get('DATABASE_URL'),
        entities: [__dirname + '/**/*.entity{.ts,.js}'],
        migrations: [__dirname + '/migrations/*{.ts,.js}'],
        synchronize: config.get('NODE_ENV') !== 'production',  // never in prod!
        ssl: config.get('NODE_ENV') === 'production' ? { rejectUnauthorized: false } : false,
      }),
      inject: [ConfigService],
    }),
  ],
})
export class AppModule {}

// Entity
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne, OneToMany } from 'typeorm';

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

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

  @Column()
  name: string;

  @Column({ select: false })  // excluded from SELECT by default
  password: string;

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

  @OneToMany(() => Order, order => order.user)
  orders: Order[];

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

// Feature module
@Module({
  imports: [TypeOrmModule.forFeature([User, Order])],
  controllers: [UsersController],
  providers: [UsersService],
  exports: [TypeOrmModule],
})
export class UsersModule {}

JWT Authentication

// auth.module.ts
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';

@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.registerAsync({
      imports: [ConfigModule],
      useFactory: (config: ConfigService) => ({
        secret: config.get('JWT_SECRET'),
        signOptions: { expiresIn: '15m' },
      }),
      inject: [ConfigService],
    }),
  ],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  controllers: [AuthController],
})
export class AuthModule {}

// auth.service.ts
@Injectable()
export class AuthService {
  constructor(
    private usersService: UsersService,
    private jwtService: JwtService,
  ) {}

  async validateUser(email: string, password: string): Promise<User | null> {
    const user = await this.usersService.findByEmail(email);
    if (user && await bcrypt.compare(password, user.password)) {
      return user;
    }
    return null;
  }

  async login(user: User) {
    const payload = { sub: user.id, email: user.email };
    return {
      access_token: this.jwtService.sign(payload),
      refresh_token: this.jwtService.sign(payload, { expiresIn: '7d' }),
    };
  }
}

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

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

  @Get('me')
  @UseGuards(JwtAuthGuard)
  getProfile(@CurrentUser() user) {
    return user;
  }
}

Swagger / OpenAPI

// main.ts
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';

const config = new DocumentBuilder()
  .setTitle('My API')
  .setDescription('API docs')
  .setVersion('1.0')
  .addBearerAuth()
  .build();

const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);  // available at /api

// DTO decorators
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';

export class CreateUserDto {
  @ApiProperty({ example: 'user@example.com' })
  @IsEmail()
  email: string;

  @ApiPropertyOptional({ description: 'Optional bio' })
  @IsOptional()
  bio?: string;
}

// Controller decorators
@ApiTags('users')
@ApiBearerAuth()
@Controller('users')
export class UsersController {
  @ApiOperation({ summary: 'Get all users' })
  @ApiResponse({ status: 200, type: [User] })
  @Get()
  findAll() { ... }
}
NestJS

Interview Questions

NestJS Interview Questions Q: What is NestJS and what problem does it solve? NestJS is a Node.js framework for building scalable server-side applications. It so

NestJS Interview Questions

Q: What is NestJS and what problem does it solve?

NestJS is a Node.js framework for building scalable server-side applications. It solves the lack of structure in vanilla Express by providing Angular-inspired architecture: modules, controllers, services, DI, and decorators. It's opinionated about organization but flexible in underlying transport (HTTP, WebSockets, microservices).

Q: What is Dependency Injection in NestJS?

NestJS has a built-in IoC container. Classes decorated with @Injectable() are managed by the container. When you inject a service via the constructor, NestJS instantiates and provides it automatically. This decouples implementations from consumers, making testing easy (inject mocks) and sharing services across modules simple.

Q: What is the request lifecycle in NestJS?

Incoming request → Middleware → Guards → Interceptors (pre) → Pipes → Route handler → Interceptors (post) → Exception filters (on error) → Response.

Q: What is the difference between a Guard and Middleware?

Middleware runs before routing and has no knowledge of which handler will be executed. Guards have access to the ExecutionContext (route handler metadata, decorators, class) — making them suitable for authorization logic that depends on what route is being accessed. Guards return true/false to allow/deny; middleware just calls next().

Q: What is an Interceptor and when would you use one?

Interceptors wrap the route handler execution using RxJS Observables. They can run code before and after the handler, transform the response, extend behavior, or handle errors. Common uses: response transformation (wrap in { data: ... }), logging request/response times, caching, serialization.

Q: How do you handle validation in NestJS?

Using the ValidationPipe (global or per-route) combined with class-validator decorators on DTOs. When whitelist: true is set, unknown properties are stripped. transform: true auto-converts plain objects to DTO class instances. For custom validation, implement ValidatorConstraint.

Q: What is the difference between forRoot() and forFeature() in TypeORM/NestJS?

forRoot() is called once in AppModule to configure the database connection (global). forFeature([Entity]) is called in each feature module to register specific entities and make their repositories injectable in that module's scope.

Q: How do you test NestJS services?

import { Test, TestingModule } from '@nestjs/testing';

describe('UsersService', () => {
  let service: UsersService;
  const mockRepo = {
    findOneBy: jest.fn(),
    save: jest.fn(),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UsersService,
        { provide: getRepositoryToken(User), useValue: mockRepo },
      ],
    }).compile();

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

  it('should throw NotFoundException if user not found', async () => {
    mockRepo.findOneBy.mockResolvedValue(null);
    await expect(service.findOne('bad-id')).rejects.toThrow(NotFoundException);
  });
});

Keep your NestJS knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever