NestJS advanced knowledge

48 min read
NestJS advanced knowledge

NestJS Interview Preparation Guide

Table of Contents

  1. Core Concepts
  2. Fundamental Principles (IoC, DI, AOP)
  3. Architecture
  4. Modules
  5. Controllers
  6. Providers & Services
  7. Dependency Injection
  8. Middleware
  9. Guards
  10. Interceptors
  11. Pipes
  12. Exception Filters
  13. Database Integration
  14. Authentication & Authorization
  15. WebSockets
  16. Testing
  17. 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:

AspectWithout IoCWith IoC
Object CreationManual (new)Container manages
CouplingTightLoose
TestingHard to mockEasy to mock
ConfigurationHardcodedCentralized

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:

TermDescriptionNestJS Example
AspectA module encapsulating cross-cutting concernInterceptor, Guard
AdviceAction taken by aspect at a join pointThe actual code in interceptor
Join PointPoint in execution where aspect can be appliedMethod execution
PointcutExpression that matches join points@UseInterceptors() on method
WeavingLinking aspects with codeNestJS 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

  1. Feature Modules - Group related features
  2. Shared Modules - Reusable across the app
  3. Global Modules - Available everywhere (@Global())
  4. 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

DecoratorDescription
@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

  1. NestJS creates a DI container at startup
  2. Providers are registered in the container
  3. 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

  • ValidationPipe
  • ParseIntPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • DefaultValuePipe
@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:

BenefitDescription
Database AbstractionSwitch databases with minimal code changes
Type SafetyTypeScript support with auto-completion
SecurityAutomatic SQL injection prevention
ProductivityLess boilerplate, cleaner code
RelationshipsEasy handling of complex relations
MigrationsVersion control for database schema

ORM Drawbacks:

DrawbackDescription
PerformanceCan generate inefficient queries
Learning CurveNeed to learn ORM-specific syntax
Complex QueriesSometimes raw SQL is easier
N+1 ProblemCan 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 TypePostgreSQLMySQLDescription
stringvarcharvarcharVariable string
texttexttextLong text
intintegerintInteger
bigintbigintbigintLarge integer
floatrealfloatFloating point
decimaldecimaldecimalPrecise decimal
booleanbooleantinyintTrue/false
datedatedateDate only
timestamptimestampdatetimeDate and time
jsonjsonjsonJSON data
jsonbjsonb-Binary JSON (Postgres)
uuiduuidvarchar(36)UUID
enumenumenumEnumeration

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

OptionDescriptionExample
cascadeAuto-save/remove related entitiescascade: true or cascade: ['insert', 'update']
eagerAuto-load relation (avoid in most cases)eager: true
lazyLoad relation on access (returns Promise)lazy: true
nullableAllow null foreign keynullable: true
onDeleteFK action on delete'CASCADE', 'SET NULL', 'RESTRICT'
onUpdateFK action on update'CASCADE', 'SET NULL', 'RESTRICT'
orphanedRowActionHandle 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

  1. 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
  2. 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
  3. Explain the module system in NestJS

    • Modules encapsulate providers, controllers
    • Enable lazy loading and code organization
    • Support dynamic configuration
  4. How does Dependency Injection work in NestJS?

    • IoC container manages instances
    • Providers registered in modules
    • Constructor injection (primary method)
    • Supports various provider types
  5. What are the different provider scopes?

    • DEFAULT (Singleton): One instance for entire app
    • REQUEST: New instance per request
    • TRANSIENT: New instance per injection
  6. How do you handle circular dependencies?

    • Use forwardRef() to resolve
    • Restructure code to avoid if possible
  7. Explain the execution order of NestJS components

    • Middleware → Guards → Interceptors (pre) → Pipes → Handler → Interceptors (post) → Exception Filters

Practical Questions

  1. 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
  2. How do you validate request data?

    • Use class-validator with DTOs
    • Apply ValidationPipe globally or per-route
    • Create custom validation pipes if needed
  3. How do you handle database transactions?

    • Use QueryRunner for manual control
    • Use @Transaction decorator (deprecated)
    • Implement repository pattern
  4. How do you implement caching?

    • Use cache-manager package
    • Apply CacheInterceptor
    • Implement custom cache strategies
  5. How do you handle file uploads?

    • Use Multer integration
    • @UseInterceptors(FileInterceptor)
    • Validate file types and sizes
  6. How do you implement rate limiting?

    • Use @nestjs/throttler package
    • Configure limits per route/controller
    • Implement custom throttle guards
  7. 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
  8. 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

#RiskDescription
A01Broken Access ControlUsers acting outside their permissions
A02Cryptographic FailuresFailures related to cryptography leading to data exposure
A03InjectionUntrusted data sent to interpreter as command/query
A04Insecure DesignMissing security controls in design phase
A05Security MisconfigurationImproperly configured security settings
A06Vulnerable ComponentsUsing components with known vulnerabilities
A07Auth FailuresBroken authentication and session management
A08Data Integrity FailuresCode/data without integrity verification
A09Logging FailuresInsufficient logging and monitoring
A10SSRFServer-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:

TypeDescriptionExample
Stored XSSMalicious script is stored in databaseComment with <script> tag saved and displayed to all users
Reflected XSSScript is reflected from server in responseURL parameter echoed back: ?search=<script>alert(1)</script>
DOM-based XSSScript manipulates DOM directlydocument.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:

AspectCSRFXSS
Attack VectorExploits user's authenticated sessionInjects malicious scripts
TargetServer-side actionsClient-side browser
User InvolvementUser must be logged inUser views malicious content
PreventionCSRF tokens, SameSite cookiesInput sanitization, CSP
Cookie AccessUses existing cookiesCan steal cookies (if not HttpOnly)

Best Practices

  1. Use DTOs for request/response validation and transformation
  2. Keep controllers thin - business logic in services
  3. Use custom decorators for repeated metadata
  4. Implement proper error handling with exception filters
  5. Write tests - unit tests for services, e2e for controllers
  6. Use environment variables for configuration
  7. Implement logging for debugging and monitoring
  8. Use transactions for data integrity
  9. Document APIs with Swagger/OpenAPI
  10. 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

Duong Ngo

Full-Stack AI Developer with 12+ years of experience

Comments