NestJS advanced knowledge

NestJS Interview Preparation Guide
Table of Contents
- Core Concepts
- Fundamental Principles (IoC, DI, AOP)
- Architecture
- Modules
- Controllers
- Providers & Services
- Dependency Injection
- Middleware
- Guards
- Interceptors
- Pipes
- Exception Filters
- Database Integration
- Authentication & Authorization
- WebSockets
- Testing
- Common Interview Questions
Core Concepts
What is NestJS?
- A progressive Node.js framework for building efficient, scalable server-side applications
- Built with TypeScript (supports JavaScript)
- Combines elements of OOP, FP, and FRP (Functional Reactive Programming)
- Inspired by Angular architecture
- Uses Express.js (default) or Fastify under the hood
Key Features
- Modular architecture
- Dependency Injection (DI) container
- TypeScript support out of the box
- Extensive CLI for scaffolding
- Built-in support for microservices
- GraphQL, WebSockets, REST API support
Fundamental Principles (IoC, DI, AOP)
1. Inversion of Control (IoC)
Definition: IoC is a design principle where the control of object creation and lifecycle is transferred from the application code to a framework or container.
Traditional Approach (Without IoC):
// YOU control object creation
class UserService {
private database: Database;
private logger: Logger;
constructor() {
// Service creates its own dependencies
this.database = new Database('localhost', 5432);
this.logger = new Logger();
}
}
// Usage - tightly coupled
const userService = new UserService();
With IoC (NestJS):
// FRAMEWORK controls object creation
@Injectable()
class UserService {
// Dependencies are provided by the container
constructor(
private database: Database,
private logger: Logger,
) {}
}
// Framework creates and manages instances
// You just declare what you need
Key Benefits:
| Aspect | Without IoC | With IoC |
|---|---|---|
| Object Creation | Manual (new) | Container manages |
| Coupling | Tight | Loose |
| Testing | Hard to mock | Easy to mock |
| Configuration | Hardcoded | Centralized |
IoC Container in NestJS:
@Module({
providers: [
UserService, // Container will create this
DatabaseService, // Container will create this
LoggerService, // Container will create this
],
})
export class AppModule {}
// NestJS container resolves dependencies automatically
2. Dependency Injection (DI)
Definition: DI is a specific technique to achieve IoC. Instead of a class creating its dependencies, they are "injected" from the outside.
Types of Dependency Injection:
// 1. Constructor Injection (Recommended in NestJS)
@Injectable()
class UserService {
constructor(
private readonly userRepository: UserRepository,
private readonly cacheService: CacheService,
) {}
}
// 2. Property Injection
@Injectable()
class UserService {
@Inject(CacheService)
private readonly cacheService: CacheService;
}
// 3. Setter Injection (Less common in NestJS)
@Injectable()
class UserService {
private cacheService: CacheService;
@Inject(CacheService)
setCacheService(cacheService: CacheService) {
this.cacheService = cacheService;
}
}
How NestJS DI Works:
1. Application starts
↓
2. NestJS scans all modules
↓
3. Identifies all @Injectable() providers
↓
4. Creates dependency graph
↓
5. Instantiates providers in correct order
↓
6. Injects dependencies into constructors
↓
7. Stores instances in container (singleton by default)
DI Tokens:
// Class-based token (most common)
constructor(private userService: UserService) {}
// String token
constructor(@Inject('API_KEY') private apiKey: string) {}
// Symbol token
const CONNECTION = Symbol('CONNECTION');
constructor(@Inject(CONNECTION) private connection: Connection) {}
IoC vs DI - The Relationship:
┌─────────────────────────────────────────────┐
│ IoC (Principle) │
│ "Don't call us, we'll call you" │
│ │
│ ┌───────────────────────────────────────┐ │
│ │ DI (Implementation) │ │
│ │ "Give me what I need" │ │
│ │ │ │
│ │ • Constructor Injection │ │
│ │ • Property Injection │ │
│ │ • Method Injection │ │
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
3. Aspect-Oriented Programming (AOP)
Definition: AOP is a programming paradigm that allows separating cross-cutting concerns (logging, security, caching, transactions) from business logic.
The Problem AOP Solves:
// WITHOUT AOP - Cross-cutting concerns scattered everywhere
class UserService {
async createUser(data: CreateUserDto) {
console.log('Creating user...', data); // Logging
if (!this.isAuthorized()) throw new Error(); // Security
const cached = await this.cache.get('users'); // Caching
const startTime = Date.now(); // Performance
// Actual business logic (only 1 line!)
const user = await this.repository.save(data);
console.log(`Took ${Date.now() - startTime}ms`); // Performance
await this.cache.invalidate('users'); // Caching
console.log('User created:', user); // Logging
return user;
}
}
WITH AOP - Clean separation:
// Business logic is clean
@Injectable()
class UserService {
@Log() // Aspect: Logging
@Authorize('admin') // Aspect: Security
@CacheInvalidate('users') // Aspect: Caching
@MeasureTime() // Aspect: Performance
async createUser(data: CreateUserDto) {
// Pure business logic only!
return this.repository.save(data);
}
}
AOP Terminology:
| Term | Description | NestJS Example |
|---|---|---|
| Aspect | A module encapsulating cross-cutting concern | Interceptor, Guard |
| Advice | Action taken by aspect at a join point | The actual code in interceptor |
| Join Point | Point in execution where aspect can be applied | Method execution |
| Pointcut | Expression that matches join points | @UseInterceptors() on method |
| Weaving | Linking aspects with code | NestJS decorator processing |
AOP Implementation in NestJS:
// 1. INTERCEPTORS - Before/After method execution
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const methodName = context.getHandler().name;
console.log(`Before: ${methodName}`);
const now = Date.now();
return next.handle().pipe(
tap(() => console.log(`After: ${methodName} - ${Date.now() - now}ms`)),
);
}
}
// 2. GUARDS - Authorization aspect
@Injectable()
export class RolesGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
const user = context.switchToHttp().getRequest().user;
return requiredRoles.some(role => user.roles.includes(role));
}
}
// 3. PIPES - Validation/Transformation aspect
@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
// Validate and transform input
return validatedValue;
}
}
// 4. EXCEPTION FILTERS - Error handling aspect
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentHost) {
// Centralized error handling
}
}
Applying AOP in NestJS:
@Controller('users')
@UseGuards(AuthGuard, RolesGuard) // Security Aspect
@UseInterceptors(LoggingInterceptor) // Logging Aspect
@UseInterceptors(CacheInterceptor) // Caching Aspect
export class UsersController {
@Post()
@Roles('admin') // Declarative security
@UsePipes(ValidationPipe) // Validation Aspect
create(@Body() dto: CreateUserDto) {
// Clean business logic - no cross-cutting concerns!
return this.usersService.create(dto);
}
}
AOP Flow in NestJS Request:
Request → Middleware → Guards → Interceptors(Pre) → Pipes → Handler → Interceptors(Post) → Response
↑ ↑ ↑ ↑ ↑
Aspect Aspect Aspect Aspect Aspect
(Logging) (Security) (Logging) (Validation) (Transform)
Summary: How IoC, DI, and AOP Work Together in NestJS
┌────────────────────────────────────────────────────────────────┐
│ NestJS Framework │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ IoC Container │ │
│ │ • Manages object lifecycle │ │
│ │ • Resolves dependency graph │ │
│ │ • Provides singleton/request/transient scopes │ │
│ │ │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ DI Mechanism │ │ AOP Features │ │ │
│ │ │ │ │ │ │ │
│ │ │ • Constructor │ │ • Guards │ │ │
│ │ │ injection │ │ • Interceptors │ │ │
│ │ │ • @Inject() │ │ • Pipes │ │ │
│ │ │ • Provider │ │ • Filters │ │ │
│ │ │ tokens │ │ • Middleware │ │ │
│ │ └─────────────────┘ └─────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ Result: Clean, testable, maintainable code │
└────────────────────────────────────────────────────────────────┘
Architecture
Request Lifecycle (Order of Execution)
Incoming Request
↓
1. Middleware
↓
2. Guards
↓
3. Interceptors (before)
↓
4. Pipes
↓
5. Route Handler (Controller)
↓
6. Interceptors (after)
↓
7. Exception Filters (if error)
↓
Response
Modules
Definition
Modules organize the application structure. Every NestJS app has at least one module (root module).
@Module({
imports: [], // Other modules this module depends on
controllers: [], // Controllers belonging to this module
providers: [], // Services/providers to be instantiated
exports: [], // Providers available to other modules
})
export class AppModule {}
Types of Modules
- Feature Modules - Group related features
- Shared Modules - Reusable across the app
- Global Modules - Available everywhere (@Global())
- Dynamic Modules - Configurable modules
Dynamic Module Example
@Module({})
export class ConfigModule {
static forRoot(options: ConfigOptions): DynamicModule {
return {
module: ConfigModule,
providers: [
{
provide: 'CONFIG_OPTIONS',
useValue: options,
},
ConfigService,
],
exports: [ConfigService],
};
}
}
Controllers
Purpose
Handle incoming requests and return responses to the client.
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
findAll(): Promise<User[]> {
return this.usersService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string): Promise<User> {
return this.usersService.findOne(id);
}
@Post()
create(@Body() createUserDto: CreateUserDto): Promise<User> {
return this.usersService.create(createUserDto);
}
@Put(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.usersService.update(id, updateUserDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.usersService.remove(id);
}
}
Common Decorators
| Decorator | Description |
|---|---|
| @Controller() | Define a controller |
| @Get(), @Post(), @Put(), @Delete(), @Patch() | HTTP methods |
| @Param() | Route parameters |
| @Query() | Query parameters |
| @Body() | Request body |
| @Headers() | Request headers |
| @Req(), @Res() | Request/Response objects |
| @HttpCode() | Set response status code |
Providers & Services
What are Providers?
- Fundamental concept in NestJS
- Can be injected as dependencies
- Include services, repositories, factories, helpers
@Injectable()
export class UsersService {
private users: User[] = [];
findAll(): User[] {
return this.users;
}
create(user: User): User {
this.users.push(user);
return user;
}
}
Provider Types
// Standard provider
providers: [UsersService]
// Value provider
providers: [{ provide: 'API_KEY', useValue: 'my-api-key' }]
// Class provider
providers: [{ provide: UsersService, useClass: MockUsersService }]
// Factory provider
providers: [{
provide: 'CONNECTION',
useFactory: (configService: ConfigService) => {
return new DatabaseConnection(configService.get('DB_HOST'));
},
inject: [ConfigService],
}]
// Existing provider (alias)
providers: [{ provide: 'AliasService', useExisting: UsersService }]
Scopes
@Injectable({ scope: Scope.DEFAULT }) // Singleton (default)
@Injectable({ scope: Scope.REQUEST }) // New instance per request
@Injectable({ scope: Scope.TRANSIENT }) // New instance each injection
Dependency Injection
How DI Works
- NestJS creates a DI container at startup
- Providers are registered in the container
- Dependencies are resolved and injected automatically
// Constructor injection (recommended)
@Injectable()
export class UsersService {
constructor(
private readonly usersRepository: UsersRepository,
private readonly configService: ConfigService,
) {}
}
// Property injection
@Injectable()
export class UsersService {
@Inject('CONFIG')
private readonly config: ConfigType;
}
Circular Dependency
// Use forwardRef() to resolve circular dependencies
@Injectable()
export class ServiceA {
constructor(
@Inject(forwardRef(() => ServiceB))
private serviceB: ServiceB,
) {}
}
Middleware
Purpose
Functions executed BEFORE the route handler. Access to request and response objects.
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log(`${req.method} ${req.url}`);
next();
}
}
// Applying middleware
@Module({
imports: [],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.exclude({ path: 'health', method: RequestMethod.GET })
.forRoutes('*');
}
}
Functional Middleware
export function logger(req: Request, res: Response, next: NextFunction) {
console.log('Request...');
next();
}
Guards
Purpose
Determine if a request should be handled by the route handler (authorization).
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean | Promise<boolean> {
const request = context.switchToHttp().getRequest();
return this.validateRequest(request);
}
private validateRequest(request: any): boolean {
// Validation logic
return !!request.headers.authorization;
}
}
Applying Guards
// Controller level
@UseGuards(AuthGuard)
@Controller('users')
export class UsersController {}
// Method level
@UseGuards(AuthGuard)
@Get()
findAll() {}
// Global level
app.useGlobalGuards(new AuthGuard());
// Module level
{
provide: APP_GUARD,
useClass: AuthGuard,
}
Role-Based Guard Example
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>('roles', [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) return true;
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
// Custom decorator
export const Roles = (...roles: Role[]) => SetMetadata('roles', roles);
// Usage
@Roles(Role.Admin)
@Get('admin')
getAdminData() {}
Interceptors
Purpose
- Transform the result returned from a function
- Transform exceptions thrown from a function
- Extend basic function behavior
- Completely override a function
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
return next.handle().pipe(
map(data => ({
success: true,
data,
timestamp: new Date().toISOString(),
})),
);
}
}
Common Use Cases
// Logging interceptor
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const now = Date.now();
return next.handle().pipe(
tap(() => console.log(`Execution time: ${Date.now() - now}ms`)),
);
}
}
// Cache interceptor
@Injectable()
export class CacheInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const isCached = true;
if (isCached) {
return of(cachedData);
}
return next.handle();
}
}
// Timeout interceptor
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(timeout(5000));
}
}
Pipes
Purpose
- Transformation: Transform input data to desired form
- Validation: Validate input data
Built-in Pipes
ValidationPipeParseIntPipeParseBoolPipeParseArrayPipeParseUUIDPipeDefaultValuePipe
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.usersService.findOne(id);
}
@Get()
findAll(@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number) {
return this.usersService.findAll(page);
}
Custom Validation Pipe with class-validator
// DTO
export class CreateUserDto {
@IsString()
@IsNotEmpty()
name: string;
@IsEmail()
email: string;
@IsInt()
@Min(0)
@Max(120)
age: number;
}
// Global validation pipe
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // Strip non-whitelisted properties
forbidNonWhitelisted: true, // Throw error for non-whitelisted
transform: true, // Auto-transform payloads to DTO instances
}));
Custom Pipe
@Injectable()
export class ParseDatePipe implements PipeTransform<string, Date> {
transform(value: string, metadata: ArgumentMetadata): Date {
const date = new Date(value);
if (isNaN(date.getTime())) {
throw new BadRequestException('Invalid date format');
}
return date;
}
}
Exception Filters
Built-in Exceptions
throw new BadRequestException('Invalid input');
throw new UnauthorizedException('Not authenticated');
throw new ForbiddenException('Access denied');
throw new NotFoundException('Resource not found');
throw new ConflictException('Resource already exists');
throw new InternalServerErrorException('Server error');
Custom Exception Filter
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentHost) {
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,
});
}
}
// Catch all exceptions
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentHost) {
// Handle all exceptions
}
}
Database Integration
What is ORM?
ORM (Object-Relational Mapping) is a technique that lets you query and manipulate data from a database using an object-oriented paradigm.
┌─────────────────────────────────────────────────────────────────┐
│ Without ORM (Raw SQL) │
├─────────────────────────────────────────────────────────────────┤
│ const result = await db.query( │
│ 'SELECT * FROM users WHERE id = $1', [userId] │
│ ); │
│ // result is raw data: { id: 1, name: 'John', ... } │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ With ORM (TypeORM) │
├─────────────────────────────────────────────────────────────────┤
│ const user = await userRepository.findOne({ │
│ where: { id: userId } │
│ }); │
│ // user is a User instance with methods and relations │
└─────────────────────────────────────────────────────────────────┘
ORM Benefits:
| Benefit | Description |
|---|---|
| Database Abstraction | Switch databases with minimal code changes |
| Type Safety | TypeScript support with auto-completion |
| Security | Automatic SQL injection prevention |
| Productivity | Less boilerplate, cleaner code |
| Relationships | Easy handling of complex relations |
| Migrations | Version control for database schema |
ORM Drawbacks:
| Drawback | Description |
|---|---|
| Performance | Can generate inefficient queries |
| Learning Curve | Need to learn ORM-specific syntax |
| Complex Queries | Sometimes raw SQL is easier |
| N+1 Problem | Can accidentally cause many queries |
TypeORM Overview
TypeORM is the most popular ORM for TypeScript/JavaScript, fully integrated with NestJS.
Key Concepts:
- Entity: A class that maps to a database table
- Repository: Object to interact with entity's table
- Migration: Version control for database schema
- QueryBuilder: Programmatic query construction
Entity Decorators
Basic Column Decorators
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
VersionColumn,
} from 'typeorm';
@Entity('users') // Table name: 'users'
export class User {
// Primary key with auto-generated UUID
@PrimaryGeneratedColumn('uuid')
id: string;
// Or auto-increment integer
@PrimaryGeneratedColumn()
id: number;
// Basic column
@Column()
name: string;
// Column with options
@Column({
type: 'varchar',
length: 255,
unique: true,
nullable: false,
default: 'anonymous',
name: 'user_name', // Custom column name in DB
})
username: string;
// Enum column
@Column({
type: 'enum',
enum: UserRole,
default: UserRole.USER,
})
role: UserRole;
// JSON column
@Column({ type: 'jsonb', nullable: true })
metadata: Record<string, any>;
// Automatic timestamps
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
// Soft delete (sets date instead of deleting)
@DeleteDateColumn()
deletedAt: Date;
// Optimistic locking version
@VersionColumn()
version: number;
}
Column Types Reference
| TypeORM Type | PostgreSQL | MySQL | Description |
|---|---|---|---|
string | varchar | varchar | Variable string |
text | text | text | Long text |
int | integer | int | Integer |
bigint | bigint | bigint | Large integer |
float | real | float | Floating point |
decimal | decimal | decimal | Precise decimal |
boolean | boolean | tinyint | True/false |
date | date | date | Date only |
timestamp | timestamp | datetime | Date and time |
json | json | json | JSON data |
jsonb | jsonb | - | Binary JSON (Postgres) |
uuid | uuid | varchar(36) | UUID |
enum | enum | enum | Enumeration |
Relationship Decorators
1. One-to-One (@OneToOne)
One record relates to exactly one record in another table.
// Example: User has one Profile
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
// One user has one profile
@OneToOne(() => Profile, (profile) => profile.user, {
cascade: true, // Auto-save related entity
eager: false, // Don't auto-load (default)
onDelete: 'CASCADE' // Delete profile when user deleted
})
@JoinColumn() // This side OWNS the relationship (has foreign key)
profile: Profile;
}
@Entity()
export class Profile {
@PrimaryGeneratedColumn()
id: number;
@Column()
bio: string;
@Column()
avatar: string;
// Inverse side (no @JoinColumn)
@OneToOne(() => User, (user) => user.profile)
user: User;
}
Database Result:
users table profiles table
+----+-------+------------+ +----+-----+--------+
| id | name | profile_id | | id | bio | avatar |
+----+-------+------------+ +----+-----+--------+
| 1 | John | 1 | | 1 | ... | ... |
+----+-------+------------+ +----+-----+--------+
2. One-to-Many / Many-to-One (@OneToMany, @ManyToOne)
One record relates to many records. Most common relationship.
// Example: One User has many Posts, each Post belongs to one User
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
// One user has many posts
@OneToMany(() => Post, (post) => post.author)
posts: Post[];
}
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column()
content: string;
// Many posts belong to one user
// This side has the foreign key (author_id)
@ManyToOne(() => User, (user) => user.posts, {
nullable: false, // Post must have an author
onDelete: 'CASCADE', // Delete posts when user deleted
onUpdate: 'CASCADE',
})
@JoinColumn({ name: 'author_id' }) // Custom FK column name
author: User;
// You can also expose the foreign key directly
@Column()
authorId: number;
}
Database Result:
users table posts table
+----+-------+ +----+-------+-----------+
| id | name | | id | title | author_id |
+----+-------+ +----+-------+-----------+
| 1 | John | | 1 | Post1 | 1 |
| 2 | Jane | | 2 | Post2 | 1 |
+----+-------+ | 3 | Post3 | 2 |
+----+-------+-----------+
3. Many-to-Many (@ManyToMany)
Many records relate to many records. Requires a junction table.
// Example: Posts can have many Tags, Tags can be on many Posts
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
// Many posts have many tags
@ManyToMany(() => Tag, (tag) => tag.posts, {
cascade: true, // Auto-save tags when saving post
})
@JoinTable({
name: 'post_tags', // Junction table name
joinColumn: {
name: 'post_id',
referencedColumnName: 'id',
},
inverseJoinColumn: {
name: 'tag_id',
referencedColumnName: 'id',
},
})
tags: Tag[];
}
@Entity()
export class Tag {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
name: string;
// Inverse side (no @JoinTable)
@ManyToMany(() => Post, (post) => post.tags)
posts: Post[];
}
Database Result:
posts table post_tags (junction) tags table
+----+-------+ +---------+--------+ +----+----------+
| id | title | | post_id | tag_id | | id | name |
+----+-------+ +---------+--------+ +----+----------+
| 1 | Post1 | | 1 | 1 | | 1 | TypeScript|
| 2 | Post2 | | 1 | 2 | | 2 | NestJS |
+----+-------+ | 2 | 2 | | 3 | Backend |
+---------+--------+ +----+----------+
4. Self-Referencing Relationships
Entity relates to itself (e.g., categories with subcategories, employees with managers).
// Example: Category with parent/children hierarchy
@Entity()
export class Category {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
// Parent category (many categories have one parent)
@ManyToOne(() => Category, (category) => category.children, {
nullable: true, // Root categories have no parent
onDelete: 'SET NULL',
})
parent: Category;
// Child categories (one category has many children)
@OneToMany(() => Category, (category) => category.parent)
children: Category[];
}
// Example: Employee with manager (tree structure)
@Entity()
export class Employee {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToOne(() => Employee, (employee) => employee.subordinates)
manager: Employee;
@OneToMany(() => Employee, (employee) => employee.manager)
subordinates: Employee[];
}
Relationship Options Reference
| Option | Description | Example |
|---|---|---|
cascade | Auto-save/remove related entities | cascade: true or cascade: ['insert', 'update'] |
eager | Auto-load relation (avoid in most cases) | eager: true |
lazy | Load relation on access (returns Promise) | lazy: true |
nullable | Allow null foreign key | nullable: true |
onDelete | FK action on delete | 'CASCADE', 'SET NULL', 'RESTRICT' |
onUpdate | FK action on update | 'CASCADE', 'SET NULL', 'RESTRICT' |
orphanedRowAction | Handle orphaned children | 'delete', 'nullify', 'disable' |
Loading Relations
// 1. Using find options
const user = await userRepository.findOne({
where: { id: 1 },
relations: ['posts', 'profile'], // Load specific relations
// Or nested relations
relations: ['posts', 'posts.comments', 'posts.tags'],
});
// 2. Using QueryBuilder (more control)
const user = await userRepository
.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'post')
.leftJoinAndSelect('post.tags', 'tag')
.where('user.id = :id', { id: 1 })
.getOne();
// 3. Lazy loading (if lazy: true)
const user = await userRepository.findOne({ where: { id: 1 } });
const posts = await user.posts; // Loads posts on access
// 4. Using relation query builder
const posts = await userRepository
.createQueryBuilder()
.relation(User, 'posts')
.of(1) // user id
.loadMany();
Avoiding N+1 Problem
// ❌ BAD: N+1 queries
const users = await userRepository.find();
for (const user of users) {
const posts = await postRepository.find({ where: { authorId: user.id } });
// This makes 1 + N queries!
}
// ✅ GOOD: Single query with join
const users = await userRepository.find({
relations: ['posts'],
});
// ✅ GOOD: QueryBuilder with select
const users = await userRepository
.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'post')
.getMany();
TypeORM Integration
// app.module.ts
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'user',
password: 'password',
database: 'mydb',
entities: [User],
synchronize: false, // Use migrations in production
}),
],
})
export class AppModule {}
// Entity
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
name: string;
@Column({ unique: true })
email: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@OneToMany(() => Post, post => post.author)
posts: Post[];
}
// Repository pattern
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
findAll(): Promise<User[]> {
return this.usersRepository.find();
}
findOne(id: string): Promise<User> {
return this.usersRepository.findOneBy({ id });
}
}
Transactions
@Injectable()
export class UsersService {
constructor(private dataSource: DataSource) {}
async createWithProfile(userData: CreateUserDto) {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const user = await queryRunner.manager.save(User, userData);
await queryRunner.manager.save(Profile, { userId: user.id });
await queryRunner.commitTransaction();
return user;
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}
}
}
Authentication & Authorization
JWT Authentication with Passport
// auth.module.ts
@Module({
imports: [
PassportModule,
JwtModule.register({
secret: 'your-secret-key',
signOptions: { expiresIn: '1h' },
}),
],
providers: [AuthService, JwtStrategy],
})
export class AuthModule {}
// jwt.strategy.ts
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: 'your-secret-key',
});
}
async validate(payload: any) {
return { userId: payload.sub, email: payload.email };
}
}
// auth.service.ts
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
) {}
async login(user: User) {
const payload = { email: user.email, sub: user.id };
return {
access_token: this.jwtService.sign(payload),
};
}
}
// Using the guard
@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@Request() req) {
return req.user;
}
WebSockets
Gateway Implementation
@WebSocketGateway({
cors: { origin: '*' },
namespace: '/chat',
})
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer()
server: Server;
handleConnection(client: Socket) {
console.log(`Client connected: ${client.id}`);
}
handleDisconnect(client: Socket) {
console.log(`Client disconnected: ${client.id}`);
}
@SubscribeMessage('message')
handleMessage(
@MessageBody() data: string,
@ConnectedSocket() client: Socket,
): void {
this.server.emit('message', data);
}
@SubscribeMessage('joinRoom')
handleJoinRoom(
@MessageBody() room: string,
@ConnectedSocket() client: Socket,
) {
client.join(room);
client.to(room).emit('userJoined', client.id);
}
}
Testing
Unit Testing
describe('UsersService', () => {
let service: UsersService;
let repository: Repository<User>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: getRepositoryToken(User),
useValue: {
find: jest.fn(),
findOne: jest.fn(),
save: jest.fn(),
},
},
],
}).compile();
service = module.get<UsersService>(UsersService);
repository = module.get<Repository<User>>(getRepositoryToken(User));
});
it('should return all users', async () => {
const users = [{ id: '1', name: 'John' }];
jest.spyOn(repository, 'find').mockResolvedValue(users as User[]);
expect(await service.findAll()).toEqual(users);
});
});
E2E Testing
describe('UsersController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
it('/users (GET)', () => {
return request(app.getHttpServer())
.get('/users')
.expect(200)
.expect('Content-Type', /json/);
});
it('/users (POST)', () => {
return request(app.getHttpServer())
.post('/users')
.send({ name: 'John', email: 'john@example.com' })
.expect(201);
});
});
Common Interview Questions
Conceptual Questions
-
What is the difference between Middleware and Interceptors?
- Middleware: Runs before guards, has access to request/response, similar to Express middleware
- Interceptors: Runs after guards, can transform response, has access to execution context
-
What is the difference between Guards and Middleware?
- Guards: For authorization, return boolean, access to ExecutionContext
- Middleware: General-purpose, runs first, no access to execution context
-
Explain the module system in NestJS
- Modules encapsulate providers, controllers
- Enable lazy loading and code organization
- Support dynamic configuration
-
How does Dependency Injection work in NestJS?
- IoC container manages instances
- Providers registered in modules
- Constructor injection (primary method)
- Supports various provider types
-
What are the different provider scopes?
- DEFAULT (Singleton): One instance for entire app
- REQUEST: New instance per request
- TRANSIENT: New instance per injection
-
How do you handle circular dependencies?
- Use
forwardRef()to resolve - Restructure code to avoid if possible
- Use
-
Explain the execution order of NestJS components
- Middleware → Guards → Interceptors (pre) → Pipes → Handler → Interceptors (post) → Exception Filters
Practical Questions
-
How do you implement authentication in NestJS?
- Use Passport.js with strategies (JWT, Local, OAuth)
- Implement guards for route protection
- Use decorators for role-based access
-
How do you validate request data?
- Use class-validator with DTOs
- Apply ValidationPipe globally or per-route
- Create custom validation pipes if needed
-
How do you handle database transactions?
- Use QueryRunner for manual control
- Use @Transaction decorator (deprecated)
- Implement repository pattern
-
How do you implement caching?
- Use cache-manager package
- Apply CacheInterceptor
- Implement custom cache strategies
-
How do you handle file uploads?
- Use Multer integration
- @UseInterceptors(FileInterceptor)
- Validate file types and sizes
-
How do you implement rate limiting?
- Use @nestjs/throttler package
- Configure limits per route/controller
- Implement custom throttle guards
-
How do you structure a large NestJS application?
- Feature-based module organization
- Shared modules for common functionality
- Domain-driven design principles
- Separate DTOs, entities, interfaces
-
How do you handle configuration?
- Use @nestjs/config
- Environment-based configuration
- Validation with Joi or class-validator
OWASP Top 10 Security Risks (2021)
The OWASP Top 10 is a standard awareness document representing the most critical security risks to web applications.
Overview
| # | Risk | Description |
|---|---|---|
| A01 | Broken Access Control | Users acting outside their permissions |
| A02 | Cryptographic Failures | Failures related to cryptography leading to data exposure |
| A03 | Injection | Untrusted data sent to interpreter as command/query |
| A04 | Insecure Design | Missing security controls in design phase |
| A05 | Security Misconfiguration | Improperly configured security settings |
| A06 | Vulnerable Components | Using components with known vulnerabilities |
| A07 | Auth Failures | Broken authentication and session management |
| A08 | Data Integrity Failures | Code/data without integrity verification |
| A09 | Logging Failures | Insufficient logging and monitoring |
| A10 | SSRF | Server-Side Request Forgery |
A01: Broken Access Control
What it is: Users can act outside their intended permissions - accessing other users' data, modifying access rights, or accessing admin functions.
Attack Examples:
// ❌ VULNERABLE: No access control check
@Get('users/:id')
getUser(@Param('id') id: string) {
return this.usersService.findOne(id); // Anyone can access any user!
}
// ❌ VULNERABLE: Insecure Direct Object Reference (IDOR)
@Get('documents/:id')
getDocument(@Param('id') id: string) {
return this.documentsService.findOne(id); // User can access others' documents
}
Protection in NestJS:
// ✅ SECURE: Implement ownership check
@UseGuards(JwtAuthGuard)
@Get('users/:id')
async getUser(@Param('id') id: string, @Request() req) {
// Only allow users to access their own data
if (req.user.id !== id && !req.user.roles.includes('admin')) {
throw new ForbiddenException('Access denied');
}
return this.usersService.findOne(id);
}
// ✅ SECURE: Resource ownership guard
@Injectable()
export class ResourceOwnerGuard implements CanActivate {
constructor(private reflector: Reflector) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const user = request.user;
const resourceId = request.params.id;
const resourceType = this.reflector.get<string>('resourceType', context.getHandler());
// Check if user owns the resource
const resource = await this.getResource(resourceType, resourceId);
return resource.userId === user.id || user.roles.includes('admin');
}
}
// Usage
@Get(':id')
@SetMetadata('resourceType', 'document')
@UseGuards(JwtAuthGuard, ResourceOwnerGuard)
getDocument(@Param('id') id: string) {
return this.documentsService.findOne(id);
}
// ✅ SECURE: Role-based access control
@Roles('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
@Delete('users/:id')
deleteUser(@Param('id') id: string) {
return this.usersService.remove(id);
}
Protection in Next.js:
// middleware.ts - Protect routes
import { NextResponse } from 'next/server';
import { getToken } from 'next-auth/jwt';
export async function middleware(request: NextRequest) {
const token = await getToken({ req: request });
// Protect admin routes
if (request.nextUrl.pathname.startsWith('/admin')) {
if (!token || token.role !== 'admin') {
return NextResponse.redirect(new URL('/unauthorized', request.url));
}
}
return NextResponse.next();
}
// API route with ownership check
// app/api/documents/[id]/route.ts
export async function GET(request: Request, { params }: { params: { id: string } }) {
const session = await getServerSession(authOptions);
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const document = await db.document.findUnique({ where: { id: params.id } });
// Check ownership
if (document.userId !== session.user.id && session.user.role !== 'admin') {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
return NextResponse.json(document);
}
A02: Cryptographic Failures
What it is: Failures related to cryptography which often lead to exposure of sensitive data (passwords, credit cards, health records, personal data).
Vulnerabilities:
- Storing passwords in plain text
- Using weak encryption algorithms (MD5, SHA1)
- Hardcoded secrets in code
- Transmitting data over HTTP
Protection in NestJS:
// ✅ SECURE: Password hashing with bcrypt
import * as bcrypt from 'bcrypt';
@Injectable()
export class AuthService {
private readonly SALT_ROUNDS = 12;
async hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, this.SALT_ROUNDS);
}
async verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
}
// ✅ SECURE: Environment-based secrets
// .env (never commit this file!)
JWT_SECRET=your-super-secret-key-at-least-32-chars
DATABASE_URL=postgres://user:pass@localhost/db
// config/configuration.ts
export default () => ({
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: '1h',
},
database: {
url: process.env.DATABASE_URL,
},
});
// ✅ SECURE: Encrypt sensitive data at rest
import * as crypto from 'crypto';
@Injectable()
export class EncryptionService {
private readonly algorithm = 'aes-256-gcm';
private readonly key = Buffer.from(process.env.ENCRYPTION_KEY, 'hex');
encrypt(text: string): { encrypted: string; iv: string; tag: string } {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(this.algorithm, this.key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return {
encrypted,
iv: iv.toString('hex'),
tag: cipher.getAuthTag().toString('hex'),
};
}
decrypt(encrypted: string, iv: string, tag: string): string {
const decipher = crypto.createDecipheriv(
this.algorithm,
this.key,
Buffer.from(iv, 'hex'),
);
decipher.setAuthTag(Buffer.from(tag, 'hex'));
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}
// ✅ SECURE: Force HTTPS in production
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
if (process.env.NODE_ENV === 'production') {
app.use((req, res, next) => {
if (req.headers['x-forwarded-proto'] !== 'https') {
return res.redirect(`https://${req.headers.host}${req.url}`);
}
next();
});
}
await app.listen(3000);
}
Protection in Next.js:
// next.config.js - Security headers
const securityHeaders = [
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload',
},
];
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: securityHeaders,
},
];
},
};
// Never expose secrets to client
// ❌ BAD - NEXT_PUBLIC_ exposes to browser
NEXT_PUBLIC_API_SECRET=secret
// ✅ GOOD - Server-only
API_SECRET=secret
A03: Injection
What it is: Untrusted data is sent to an interpreter as part of a command or query (SQL, NoSQL, OS, LDAP injection).
Protection in NestJS:
// ✅ SECURE: Parameterized queries with TypeORM
@Injectable()
export class UsersService {
// Using repository (automatically parameterized)
async findByEmail(email: string): Promise<User> {
return this.usersRepository.findOne({ where: { email } });
}
// Using QueryBuilder (parameterized)
async searchUsers(searchTerm: string): Promise<User[]> {
return this.usersRepository
.createQueryBuilder('user')
.where('user.name ILIKE :search', { search: `%${searchTerm}%` })
.getMany();
}
// ❌ VULNERABLE: String interpolation
async unsafeSearch(searchTerm: string): Promise<User[]> {
return this.usersRepository.query(
`SELECT * FROM users WHERE name LIKE '%${searchTerm}%'` // SQL INJECTION!
);
}
}
// ✅ SECURE: Input validation with class-validator
export class SearchDto {
@IsString()
@MaxLength(100)
@Matches(/^[a-zA-Z0-9\s]+$/, { message: 'Invalid characters in search' })
query: string;
}
// ✅ SECURE: NoSQL injection prevention
import * as mongoSanitize from 'express-mongo-sanitize';
// main.ts
app.use(mongoSanitize()); // Removes $ and . from req.body, req.query, req.params
// Or manually sanitize
function sanitizeMongoQuery(obj: any): any {
if (typeof obj !== 'object' || obj === null) return obj;
const sanitized: any = {};
for (const key in obj) {
if (!key.startsWith('$') && !key.includes('.')) {
sanitized[key] = typeof obj[key] === 'object'
? sanitizeMongoQuery(obj[key])
: obj[key];
}
}
return sanitized;
}
// ✅ SECURE: Command injection prevention
import { execFile } from 'child_process';
// ❌ VULNERABLE
exec(`convert ${userInput} output.png`); // Command injection!
// ✅ SECURE - Use execFile with arguments array
execFile('convert', [userInput, 'output.png'], (error, stdout) => {
// Safe - arguments are escaped
});
Protection in Next.js:
// app/api/search/route.ts
import { z } from 'zod';
const searchSchema = z.object({
query: z.string().max(100).regex(/^[a-zA-Z0-9\s]+$/),
});
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const result = searchSchema.safeParse({
query: searchParams.get('query'),
});
if (!result.success) {
return NextResponse.json({ error: 'Invalid input' }, { status: 400 });
}
// Use parameterized query
const users = await prisma.user.findMany({
where: {
name: { contains: result.data.query, mode: 'insensitive' },
},
});
return NextResponse.json(users);
}
A04: Insecure Design
What it is: Missing or ineffective security controls designed into the application from the start. It's about flaws in design, not implementation.
Examples of Insecure Design:
- No rate limiting on sensitive operations
- No account lockout after failed logins
- Password reset via security questions only
- No limits on resource creation
Protection in NestJS:
// ✅ SECURE: Rate limiting with @nestjs/throttler
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
@Module({
imports: [
ThrottlerModule.forRoot([
{
name: 'short',
ttl: 1000, // 1 second
limit: 3, // 3 requests per second
},
{
name: 'long',
ttl: 60000, // 1 minute
limit: 100, // 100 requests per minute
},
]),
],
providers: [
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
})
export class AppModule {}
// Custom throttle for login attempts
@Controller('auth')
export class AuthController {
@Post('login')
@Throttle({ default: { limit: 5, ttl: 60000 } }) // 5 attempts per minute
async login(@Body() loginDto: LoginDto) {
return this.authService.login(loginDto);
}
}
// ✅ SECURE: Account lockout mechanism
@Injectable()
export class AuthService {
private readonly MAX_FAILED_ATTEMPTS = 5;
private readonly LOCKOUT_DURATION = 15 * 60 * 1000; // 15 minutes
async validateUser(email: string, password: string): Promise<User | null> {
const user = await this.usersService.findByEmail(email);
if (!user) return null;
// Check if account is locked
if (user.lockedUntil && user.lockedUntil > new Date()) {
throw new ForbiddenException(
`Account locked. Try again after ${user.lockedUntil.toISOString()}`
);
}
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
// Increment failed attempts
const failedAttempts = user.failedLoginAttempts + 1;
const updates: Partial<User> = { failedLoginAttempts: failedAttempts };
if (failedAttempts >= this.MAX_FAILED_ATTEMPTS) {
updates.lockedUntil = new Date(Date.now() + this.LOCKOUT_DURATION);
updates.failedLoginAttempts = 0;
}
await this.usersService.update(user.id, updates);
return null;
}
// Reset failed attempts on success
await this.usersService.update(user.id, {
failedLoginAttempts: 0,
lockedUntil: null
});
return user;
}
}
// ✅ SECURE: Resource creation limits
@Injectable()
export class PostsService {
private readonly MAX_POSTS_PER_DAY = 10;
async create(userId: string, createPostDto: CreatePostDto): Promise<Post> {
// Check daily limit
const todayStart = new Date();
todayStart.setHours(0, 0, 0, 0);
const postsToday = await this.postsRepository.count({
where: {
authorId: userId,
createdAt: MoreThanOrEqual(todayStart),
},
});
if (postsToday >= this.MAX_POSTS_PER_DAY) {
throw new BadRequestException('Daily post limit reached');
}
return this.postsRepository.save({ ...createPostDto, authorId: userId });
}
}
Protection in Next.js:
// Rate limiting with upstash/ratelimit
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(5, '1 m'), // 5 requests per minute
});
// app/api/auth/login/route.ts
export async function POST(request: Request) {
const ip = request.headers.get('x-forwarded-for') ?? '127.0.0.1';
const { success, remaining } = await ratelimit.limit(ip);
if (!success) {
return NextResponse.json(
{ error: 'Too many requests' },
{ status: 429, headers: { 'X-RateLimit-Remaining': remaining.toString() } }
);
}
// Process login...
}
A05: Security Misconfiguration
What it is: Missing security hardening, unnecessary features enabled, default accounts, overly verbose error messages.
Protection in NestJS:
// main.ts - Security configuration
import helmet from 'helmet';
import * as compression from 'compression';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: process.env.NODE_ENV === 'production'
? ['error', 'warn']
: ['log', 'error', 'warn', 'debug'],
});
// ✅ Security headers with Helmet
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:'],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
},
}));
// ✅ CORS configuration
app.enableCors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
credentials: true,
});
// ✅ Disable X-Powered-By header
app.getHttpAdapter().getInstance().disable('x-powered-by');
// ✅ Global validation pipe
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
disableErrorMessages: process.env.NODE_ENV === 'production',
}));
// ✅ Global exception filter (hide internal errors)
app.useGlobalFilters(new GlobalExceptionFilter());
await app.listen(process.env.PORT || 3000);
}
// ✅ SECURE: Custom exception filter for production
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
let status = 500;
let message = 'Internal server error';
if (exception instanceof HttpException) {
status = exception.getStatus();
message = exception.message;
} else {
// Log the actual error internally
console.error('Unhandled exception:', exception);
// Don't expose internal errors to client in production
if (process.env.NODE_ENV !== 'production') {
message = exception instanceof Error ? exception.message : 'Unknown error';
}
}
response.status(status).json({
statusCode: status,
message,
timestamp: new Date().toISOString(),
});
}
}
Protection in Next.js:
// next.config.js
const securityHeaders = [
{ key: 'X-DNS-Prefetch-Control', value: 'on' },
{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'origin-when-cross-origin' },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
{
key: 'Content-Security-Policy',
value: `
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' blob: data: https:;
font-src 'self';
connect-src 'self' ${process.env.NEXT_PUBLIC_API_URL};
`.replace(/\n/g, ''),
},
];
module.exports = {
poweredByHeader: false, // Remove X-Powered-By
async headers() {
return [{ source: '/(.*)', headers: securityHeaders }];
},
};
// ✅ Error handling - Don't expose stack traces
// app/error.tsx
'use client';
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
// Log error to monitoring service
useEffect(() => {
console.error(error);
// sendToErrorTracking(error);
}, [error]);
return (
<div>
<h2>Something went wrong!</h2>
{/* Don't show error.message in production */}
<button onClick={() => reset()}>Try again</button>
</div>
);
}
A06: Vulnerable and Outdated Components
What it is: Using components (libraries, frameworks) with known vulnerabilities or not keeping them updated.
Protection:
# ✅ Regular security audits
npm audit
npm audit fix
# ✅ Check for outdated packages
npm outdated
# ✅ Use automated tools
# Add to CI/CD pipeline
npx snyk test
npx retire
# ✅ Lock file - commit package-lock.json
git add package-lock.json
# ✅ Use dependabot or renovate for automated updates
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
// ✅ Check versions programmatically
// scripts/check-vulnerabilities.ts
import { execSync } from 'child_process';
const result = execSync('npm audit --json').toString();
const audit = JSON.parse(result);
if (audit.metadata.vulnerabilities.high > 0 || audit.metadata.vulnerabilities.critical > 0) {
console.error('Critical vulnerabilities found!');
process.exit(1);
}
A07: Identification and Authentication Failures
What it is: Weaknesses in authentication mechanisms allowing attackers to compromise passwords, keys, or session tokens.
Protection in NestJS:
// ✅ SECURE: Strong password requirements
import { IsString, MinLength, Matches } from 'class-validator';
export class RegisterDto {
@IsString()
@MinLength(12, { message: 'Password must be at least 12 characters' })
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/, {
message: 'Password must include uppercase, lowercase, number, and special character',
})
password: string;
}
// ✅ SECURE: JWT with refresh tokens
@Injectable()
export class AuthService {
async login(user: User) {
const payload = { sub: user.id, email: user.email };
const accessToken = this.jwtService.sign(payload, {
secret: process.env.JWT_ACCESS_SECRET,
expiresIn: '15m', // Short-lived
});
const refreshToken = this.jwtService.sign(payload, {
secret: process.env.JWT_REFRESH_SECRET,
expiresIn: '7d',
});
// Store refresh token hash in database
await this.saveRefreshToken(user.id, refreshToken);
return { accessToken, refreshToken };
}
async refresh(refreshToken: string) {
try {
const payload = this.jwtService.verify(refreshToken, {
secret: process.env.JWT_REFRESH_SECRET,
});
// Verify token exists in database (can be revoked)
const storedToken = await this.getRefreshToken(payload.sub);
if (!storedToken || !await bcrypt.compare(refreshToken, storedToken.hash)) {
throw new UnauthorizedException('Invalid refresh token');
}
// Issue new tokens
return this.login(await this.usersService.findOne(payload.sub));
} catch {
throw new UnauthorizedException('Invalid refresh token');
}
}
async logout(userId: string) {
// Invalidate refresh token
await this.deleteRefreshToken(userId);
}
}
// ✅ SECURE: Session configuration
@Module({
imports: [
JwtModule.registerAsync({
useFactory: (configService: ConfigService) => ({
secret: configService.get('JWT_SECRET'),
signOptions: {
expiresIn: '15m',
algorithm: 'HS256',
},
}),
inject: [ConfigService],
}),
],
})
export class AuthModule {}
// ✅ SECURE: MFA implementation
@Injectable()
export class MfaService {
generateSecret(): { secret: string; qrCode: string } {
const secret = speakeasy.generateSecret({
name: 'MyApp',
length: 32,
});
return {
secret: secret.base32,
qrCode: secret.otpauth_url,
};
}
verifyToken(secret: string, token: string): boolean {
return speakeasy.totp.verify({
secret,
encoding: 'base32',
token,
window: 1, // Allow 1 step tolerance
});
}
}
A08: Software and Data Integrity Failures
What it is: Code and infrastructure that doesn't protect against integrity violations (unverified updates, insecure CI/CD, unverified serialization).
Protection in NestJS:
// ✅ SECURE: Verify webhook signatures
@Controller('webhooks')
export class WebhooksController {
@Post('stripe')
async handleStripeWebhook(
@Headers('stripe-signature') signature: string,
@Req() request: RawBodyRequest<Request>,
) {
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
try {
const event = stripe.webhooks.constructEvent(
request.rawBody,
signature,
webhookSecret,
);
// Process verified event
return this.processWebhookEvent(event);
} catch (err) {
throw new BadRequestException('Invalid webhook signature');
}
}
}
// ✅ SECURE: Signed cookies
// main.ts
app.use(cookieParser(process.env.COOKIE_SECRET));
// Setting signed cookie
response.cookie('session', data, {
signed: true,
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
});
// ✅ SECURE: Subresource Integrity (SRI) for CDN resources
// In HTML templates
<script
src="https://cdn.example.com/lib.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"
></script>
Protection in Next.js:
// next.config.js - Subresource Integrity
module.exports = {
experimental: {
sri: {
algorithm: 'sha384',
},
},
};
// ✅ Verify API responses
async function fetchWithIntegrity(url: string, expectedHash: string) {
const response = await fetch(url);
const data = await response.text();
const hash = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(data),
);
const hashHex = Array.from(new Uint8Array(hash))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
if (hashHex !== expectedHash) {
throw new Error('Data integrity check failed');
}
return JSON.parse(data);
}
A09: Security Logging and Monitoring Failures
What it is: Without logging and monitoring, breaches cannot be detected. Insufficient logging makes forensics impossible.
Protection in NestJS:
// ✅ SECURE: Comprehensive logging interceptor
@Injectable()
export class SecurityLoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger('SecurityAudit');
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const { method, url, ip, user, body } = request;
const startTime = Date.now();
// Log request (sanitize sensitive data)
this.logger.log({
type: 'REQUEST',
method,
url,
ip,
userId: user?.id,
timestamp: new Date().toISOString(),
});
return next.handle().pipe(
tap(() => {
// Log successful response
this.logger.log({
type: 'RESPONSE',
method,
url,
userId: user?.id,
duration: Date.now() - startTime,
status: 'SUCCESS',
});
}),
catchError((error) => {
// Log errors
this.logger.error({
type: 'ERROR',
method,
url,
userId: user?.id,
duration: Date.now() - startTime,
error: error.message,
stack: error.stack,
});
throw error;
}),
);
}
}
// ✅ SECURE: Security event logging service
@Injectable()
export class SecurityAuditService {
private readonly logger = new Logger('SecurityAudit');
logLoginAttempt(email: string, success: boolean, ip: string) {
this.logger.log({
event: 'LOGIN_ATTEMPT',
email,
success,
ip,
timestamp: new Date().toISOString(),
});
// Alert on multiple failures
if (!success) {
this.checkForBruteForce(email, ip);
}
}
logSensitiveDataAccess(userId: string, resource: string, action: string) {
this.logger.log({
event: 'SENSITIVE_ACCESS',
userId,
resource,
action,
timestamp: new Date().toISOString(),
});
}
logPrivilegeChange(adminId: string, targetUserId: string, changes: any) {
this.logger.warn({
event: 'PRIVILEGE_CHANGE',
adminId,
targetUserId,
changes,
timestamp: new Date().toISOString(),
});
}
private async checkForBruteForce(email: string, ip: string) {
// Check failed attempts in last 15 minutes
const recentFailures = await this.getRecentFailures(email, ip);
if (recentFailures > 10) {
this.logger.error({
event: 'BRUTE_FORCE_DETECTED',
email,
ip,
attempts: recentFailures,
timestamp: new Date().toISOString(),
});
// Send alert to security team
await this.alertSecurityTeam({ email, ip, attempts: recentFailures });
}
}
}
// ✅ Use in auth service
@Injectable()
export class AuthService {
constructor(private securityAudit: SecurityAuditService) {}
async login(email: string, password: string, ip: string) {
const user = await this.validateUser(email, password);
this.securityAudit.logLoginAttempt(email, !!user, ip);
if (!user) {
throw new UnauthorizedException();
}
return this.generateTokens(user);
}
}
A10: Server-Side Request Forgery (SSRF)
What it is: Attacker can make the server perform requests to unintended locations, potentially accessing internal services.
Attack Example:
// ❌ VULNERABLE: User controls URL
@Get('fetch')
async fetchUrl(@Query('url') url: string) {
const response = await fetch(url); // Can access internal services!
return response.json();
}
// Attacker uses: ?url=http://169.254.169.254/latest/meta-data/ (AWS metadata)
// Or: ?url=http://localhost:6379/ (Internal Redis)
Protection in NestJS:
// ✅ SECURE: URL validation and allowlist
import { URL } from 'url';
import * as dns from 'dns/promises';
import * as ipaddr from 'ipaddr.js';
@Injectable()
export class SafeFetchService {
private readonly ALLOWED_DOMAINS = ['api.example.com', 'cdn.example.com'];
private readonly BLOCKED_IPS = [
'127.0.0.0/8', // Localhost
'10.0.0.0/8', // Private
'172.16.0.0/12', // Private
'192.168.0.0/16', // Private
'169.254.0.0/16', // Link-local (AWS metadata)
'0.0.0.0/8', // Current network
];
async safeFetch(urlString: string): Promise<any> {
// Parse and validate URL
let url: URL;
try {
url = new URL(urlString);
} catch {
throw new BadRequestException('Invalid URL');
}
// Only allow HTTPS
if (url.protocol !== 'https:') {
throw new BadRequestException('Only HTTPS URLs are allowed');
}
// Check against allowlist
if (!this.ALLOWED_DOMAINS.includes(url.hostname)) {
throw new BadRequestException('Domain not allowed');
}
// Resolve DNS and check for internal IPs
const addresses = await dns.resolve4(url.hostname);
for (const address of addresses) {
if (this.isBlockedIP(address)) {
throw new BadRequestException('Access to internal resources is not allowed');
}
}
// Safe to fetch
const response = await fetch(urlString, {
timeout: 5000,
redirect: 'error', // Don't follow redirects
});
return response.json();
}
private isBlockedIP(ip: string): boolean {
const addr = ipaddr.parse(ip);
for (const range of this.BLOCKED_IPS) {
const [network, bits] = range.split('/');
if (addr.match(ipaddr.parse(network), parseInt(bits))) {
return true;
}
}
return false;
}
}
// Controller usage
@Controller('proxy')
export class ProxyController {
constructor(private safeFetch: SafeFetchService) {}
@Get()
async fetch(@Query('url') url: string) {
return this.safeFetch.safeFetch(url);
}
}
Protection in Next.js:
// app/api/fetch/route.ts
const ALLOWED_DOMAINS = ['api.example.com'];
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const targetUrl = searchParams.get('url');
if (!targetUrl) {
return NextResponse.json({ error: 'URL required' }, { status: 400 });
}
try {
const url = new URL(targetUrl);
// Validate protocol
if (url.protocol !== 'https:') {
return NextResponse.json({ error: 'Only HTTPS allowed' }, { status: 400 });
}
// Validate domain
if (!ALLOWED_DOMAINS.includes(url.hostname)) {
return NextResponse.json({ error: 'Domain not allowed' }, { status: 400 });
}
const response = await fetch(targetUrl);
const data = await response.json();
return NextResponse.json(data);
} catch {
return NextResponse.json({ error: 'Invalid request' }, { status: 400 });
}
}
OWASP Security Checklist
✅ Broken Access Control
□ Implement role-based access control (RBAC)
□ Verify resource ownership
□ Deny by default
□ Log access control failures
✅ Cryptographic Failures
□ Use bcrypt/argon2 for passwords
□ Use strong encryption (AES-256)
□ Store secrets in environment variables
□ Enforce HTTPS
✅ Injection
□ Use parameterized queries
□ Validate and sanitize all inputs
□ Use ORMs properly
□ Escape outputs
✅ Insecure Design
□ Implement rate limiting
□ Add account lockout
□ Design with threat modeling
□ Limit resource creation
✅ Security Misconfiguration
□ Use security headers (Helmet)
□ Disable verbose errors in production
□ Configure CORS properly
□ Remove default credentials
✅ Vulnerable Components
□ Run npm audit regularly
□ Keep dependencies updated
□ Use automated security scanning
□ Monitor for vulnerabilities
✅ Authentication Failures
□ Enforce strong passwords
□ Implement MFA
□ Use secure session management
□ Implement proper logout
✅ Data Integrity Failures
□ Verify digital signatures
□ Use signed cookies
□ Implement SRI for CDN resources
□ Secure CI/CD pipeline
✅ Logging Failures
□ Log security events
□ Monitor for anomalies
□ Implement alerting
□ Protect log integrity
✅ SSRF
□ Validate and sanitize URLs
□ Use allowlists
□ Block internal IP ranges
□ Disable redirects
✅ XSS (Cross-Site Scripting)
□ Sanitize user inputs
□ Encode outputs properly
□ Use Content Security Policy (CSP)
□ Use HttpOnly cookies
✅ CSRF (Cross-Site Request Forgery)
□ Implement CSRF tokens
□ Use SameSite cookie attribute
□ Validate Origin/Referer headers
□ Require re-authentication for sensitive actions
XSS (Cross-Site Scripting)
Concept: XSS occurs when an attacker injects malicious scripts into web pages viewed by other users. The script executes in the victim's browser with the same privileges as the legitimate site.
Types of XSS:
| Type | Description | Example |
|---|---|---|
| Stored XSS | Malicious script is stored in database | Comment with <script> tag saved and displayed to all users |
| Reflected XSS | Script is reflected from server in response | URL parameter echoed back: ?search=<script>alert(1)</script> |
| DOM-based XSS | Script manipulates DOM directly | document.innerHTML = location.hash |
Solutions in NestJS:
// 1. Use Helmet to set security headers (including CSP)
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
},
},
}));
// 2. Sanitize user inputs with class-validator and sanitize-html
import * as sanitizeHtml from 'sanitize-html';
@Injectable()
export class SanitizePipe implements PipeTransform {
transform(value: any) {
if (typeof value === 'string') {
return sanitizeHtml(value, {
allowedTags: [], // Strip all HTML tags
allowedAttributes: {},
});
}
return value;
}
}
// 3. Use validation decorators
import { IsString, IsNotEmpty } from 'class-validator';
export class CreateCommentDto {
@IsString()
@IsNotEmpty()
@Transform(({ value }) => sanitizeHtml(value, { allowedTags: [] }))
content: string;
}
// 4. Set HttpOnly cookies to prevent script access
res.cookie('session', token, {
httpOnly: true, // JavaScript cannot access this cookie
secure: true, // Only sent over HTTPS
sameSite: 'strict',
});
Solutions in Next.js/React:
// 1. React automatically escapes content (safe by default)
const userInput = '<script>alert("xss")</script>';
return <div>{userInput}</div>; // Renders as text, not executed
// 2. DANGER: Avoid dangerouslySetInnerHTML without sanitization
// BAD - vulnerable
<div dangerouslySetInnerHTML={{ __html: userInput }} />
// GOOD - sanitize first
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />
// 3. Set CSP headers in next.config.js
const securityHeaders = [
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';",
},
];
module.exports = {
async headers() {
return [{ source: '/(.*)', headers: securityHeaders }];
},
};
// 4. Sanitize URL parameters
import { useSearchParams } from 'next/navigation';
export default function SearchPage() {
const searchParams = useSearchParams();
const query = searchParams.get('q');
// Validate and sanitize before use
const sanitizedQuery = query?.replace(/<[^>]*>/g, '') ?? '';
return <div>Search results for: {sanitizedQuery}</div>;
}
CSRF (Cross-Site Request Forgery)
Concept: CSRF tricks authenticated users into performing unintended actions on a web application. The attacker exploits the user's authenticated session to make requests without their knowledge.
Attack Flow:
1. User logs into bank.com (session cookie set)
2. User visits malicious-site.com (in another tab)
3. Malicious site contains: <img src="bank.com/transfer?to=attacker&amount=1000">
4. Browser automatically sends bank.com cookies with request
5. Bank processes transfer because user is authenticated
Solutions in NestJS:
// 1. Install and configure csurf package
import * as csurf from 'csurf';
import * as cookieParser from 'cookie-parser';
// Enable cookie parser first
app.use(cookieParser());
// Enable CSRF protection
app.use(csurf({
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
}
}));
// 2. Create CSRF token endpoint
@Controller('csrf')
export class CsrfController {
@Get('token')
getCsrfToken(@Req() req: Request) {
return { csrfToken: req.csrfToken() };
}
}
// 3. Validate CSRF token in a Guard
@Injectable()
export class CsrfGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const csrfToken = request.headers['x-csrf-token'];
if (!csrfToken || csrfToken !== request.cookies['_csrf']) {
throw new ForbiddenException('Invalid CSRF token');
}
return true;
}
}
// 4. Use SameSite cookies
res.cookie('session', token, {
httpOnly: true,
secure: true,
sameSite: 'strict', // Prevents cookie from being sent cross-site
});
// 5. Validate Origin/Referer headers
@Injectable()
export class OriginGuard implements CanActivate {
private readonly allowedOrigins = ['https://myapp.com'];
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const origin = request.headers.origin || request.headers.referer;
if (!origin || !this.allowedOrigins.some(o => origin.startsWith(o))) {
throw new ForbiddenException('Invalid origin');
}
return true;
}
}
Solutions in Next.js/React:
// 1. Fetch CSRF token on app load
// lib/csrf.ts
let csrfToken: string | null = null;
export async function getCsrfToken(): Promise<string> {
if (!csrfToken) {
const res = await fetch('/api/csrf/token', { credentials: 'include' });
const data = await res.json();
csrfToken = data.csrfToken;
}
return csrfToken!;
}
// 2. Include CSRF token in all state-changing requests
export async function apiPost(url: string, data: any) {
const token = await getCsrfToken();
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': token,
},
credentials: 'include',
body: JSON.stringify(data),
});
}
// 3. Create an Axios instance with CSRF interceptor
import axios from 'axios';
const api = axios.create({
baseURL: '/api',
withCredentials: true,
});
api.interceptors.request.use(async (config) => {
if (['post', 'put', 'patch', 'delete'].includes(config.method!)) {
const token = await getCsrfToken();
config.headers['X-CSRF-Token'] = token;
}
return config;
});
// 4. For Next.js API routes - implement CSRF middleware
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Skip CSRF check for GET, HEAD, OPTIONS
if (['GET', 'HEAD', 'OPTIONS'].includes(request.method)) {
return NextResponse.next();
}
const csrfToken = request.headers.get('x-csrf-token');
const cookieToken = request.cookies.get('csrf-token')?.value;
if (!csrfToken || csrfToken !== cookieToken) {
return NextResponse.json(
{ error: 'Invalid CSRF token' },
{ status: 403 }
);
}
return NextResponse.next();
}
export const config = {
matcher: '/api/:path*',
};
// 5. Double Submit Cookie Pattern
// Generate token on page load
export async function GET() {
const token = crypto.randomUUID();
const response = NextResponse.json({ csrfToken: token });
response.cookies.set('csrf-token', token, {
httpOnly: false, // Must be readable by JavaScript
secure: true,
sameSite: 'strict',
});
return response;
}
CSRF vs XSS Comparison:
| Aspect | CSRF | XSS |
|---|---|---|
| Attack Vector | Exploits user's authenticated session | Injects malicious scripts |
| Target | Server-side actions | Client-side browser |
| User Involvement | User must be logged in | User views malicious content |
| Prevention | CSRF tokens, SameSite cookies | Input sanitization, CSP |
| Cookie Access | Uses existing cookies | Can steal cookies (if not HttpOnly) |
Best Practices
- Use DTOs for request/response validation and transformation
- Keep controllers thin - business logic in services
- Use custom decorators for repeated metadata
- Implement proper error handling with exception filters
- Write tests - unit tests for services, e2e for controllers
- Use environment variables for configuration
- Implement logging for debugging and monitoring
- Use transactions for data integrity
- Document APIs with Swagger/OpenAPI
- Follow SOLID principles in architecture
Quick Reference Commands
# Create new project
nest new project-name
# Generate resources
nest g module users
nest g controller users
nest g service users
nest g resource users # Full CRUD
# Run application
npm run start:dev
# Run tests
npm run test
npm run test:e2e
npm run test:cov
Good luck with your interview! 🚀
Duong Ngo
Full-Stack AI Developer with 12+ years of experience