Skip to main content

Chapter 10: Advanced Patterns

Mastering advanced patterns unlocks the full power of NestJS. This chapter covers CQRS, GraphQL, WebSockets, event sourcing, and real-world case studies for building scalable, modern applications. We’ll walk through practical examples and explain when to use each pattern.

10.1 CQRS (Command Query Responsibility Segregation)

CQRS separates read and write operations for better scalability and maintainability. Use the @nestjs/cqrs package for implementing CQRS in NestJS.

What is CQRS?

CQRS separates:
  • Commands: Write operations (create, update, delete)
  • Queries: Read operations (get, list, search)
Benefits:
  • Optimize reads and writes independently
  • Scale read and write models separately
  • Simplify complex domains
  • Better performance
Analogy:
Think of CQRS as having two separate desks: one for taking orders (commands), and one for answering questions (queries). Each can be optimized for its specific purpose.

Installation

npm install @nestjs/cqrs

Command Example

// users/commands/create-user.command.ts
export class CreateUserCommand {
  constructor(
    public readonly name: string,
    public readonly email: string,
  ) {}
}

// users/commands/handlers/create-user.handler.ts
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { CreateUserCommand } from '../create-user.command';
import { UsersService } from '../../users.service';

@CommandHandler(CreateUserCommand)
export class CreateUserHandler implements ICommandHandler<CreateUserCommand> {
  constructor(private usersService: UsersService) {}

  async execute(command: CreateUserCommand) {
    const { name, email } = command;
    return this.usersService.create({ name, email });
  }
}

Query Example

// users/queries/get-user.query.ts
export class GetUserQuery {
  constructor(public readonly id: number) {}
}

// users/queries/handlers/get-user.handler.ts
import { QueryHandler, IQueryHandler } from '@nestjs/cqrs';
import { GetUserQuery } from '../get-user.query';
import { UsersService } from '../../users.service';

@QueryHandler(GetUserQuery)
export class GetUserHandler implements IQueryHandler<GetUserQuery> {
  constructor(private usersService: UsersService) {}

  async execute(query: GetUserQuery) {
    return this.usersService.findOne(query.id);
  }
}

Using in Controller

// users/users.controller.ts
import { Controller, Post, Get, Body, Param } from '@nestjs/common';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { CreateUserCommand } from './commands/create-user.command';
import { GetUserQuery } from './queries/get-user.query';

@Controller('users')
export class UsersController {
  constructor(
    private commandBus: CommandBus,
    private queryBus: QueryBus,
  ) {}

  @Post()
  async create(@Body() dto: CreateUserDto) {
    return this.commandBus.execute(
      new CreateUserCommand(dto.name, dto.email),
    );
  }

  @Get(':id')
  async findOne(@Param('id') id: number) {
    return this.queryBus.execute(new GetUserQuery(id));
  }
}

Module Setup

// users/users.module.ts
import { Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import { UsersController } from './users.controller';
import { CreateUserHandler } from './commands/handlers/create-user.handler';
import { GetUserHandler } from './queries/handlers/get-user.handler';

@Module({
  imports: [CqrsModule],
  controllers: [UsersController],
  providers: [
    CreateUserHandler,
    GetUserHandler,
    // ... other handlers
  ],
})
export class UsersModule {}
Best Practices:
  • Use separate models for reads and writes
  • Implement event sourcing for auditability
  • Use CQRS for complex domains, not simple CRUD
  • Keep commands and queries focused

10.2 GraphQL APIs

NestJS supports GraphQL out of the box. Use code-first or schema-first approaches. GraphQL lets clients request exactly the data they need.

Installation

npm install @nestjs/graphql @nestjs/apollo graphql apollo-server-express

Code-First Approach

// users/entities/user.entity.ts
import { ObjectType, Field, Int } from '@nestjs/graphql';

@ObjectType()
export class User {
  @Field(() => Int)
  id: number;

  @Field()
  name: string;

  @Field()
  email: string;
}

Resolver

// users/users.resolver.ts
import { Resolver, Query, Mutation, Args, Int } from '@nestjs/graphql';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';
import { CreateUserInput } from './dto/create-user.input';

@Resolver(() => User)
export class UsersResolver {
  constructor(private usersService: UsersService) {}

  @Query(() => [User], { name: 'users' })
  findAll() {
    return this.usersService.findAll();
  }

  @Query(() => User, { name: 'user' })
  findOne(@Args('id', { type: () => Int }) id: number) {
    return this.usersService.findOne(id);
  }

  @Mutation(() => User)
  createUser(@Args('createUserInput') createUserInput: CreateUserInput) {
    return this.usersService.create(createUserInput);
  }
}

Input DTO

// users/dto/create-user.input.ts
import { InputType, Field } from '@nestjs/graphql';

@InputType()
export class CreateUserInput {
  @Field()
  name: string;

  @Field()
  email: string;
}

Module Setup

// app.module.ts
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { join } from 'path';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
      sortSchema: true,
    }),
    UsersModule,
  ],
})
export class AppModule {}
Diagram: GraphQL Flow
Client → GraphQL Query/Mutation

GraphQL Resolver

Service

Repository → Database

Response (only requested fields)
Best Practices:
  • Use DTOs for input validation
  • Leverage GraphQL subscriptions for real-time updates
  • Document your schema for frontend teams
  • Use DataLoader for N+1 query optimization

10.3 WebSockets & Real-Time Communication

WebSockets enable real-time, bidirectional communication. Use @nestjs/websockets for gateways and event handling.

Installation

npm install @nestjs/websockets @nestjs/platform-socket.io socket.io

WebSocket Gateway

// chat/chat.gateway.ts
import {
  WebSocketGateway,
  WebSocketServer,
  SubscribeMessage,
  OnGatewayConnection,
  OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';

@WebSocketGateway({
  cors: {
    origin: '*',
  },
})
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(client: Socket, payload: any) {
    this.server.emit('message', payload);
  }

  @SubscribeMessage('join_room')
  handleJoinRoom(client: Socket, room: string) {
    client.join(room);
    this.server.to(room).emit('user_joined', client.id);
  }

  @SubscribeMessage('leave_room')
  handleLeaveRoom(client: Socket, room: string) {
    client.leave(room);
    this.server.to(room).emit('user_left', client.id);
  }
}

Using with Authentication

@WebSocketGateway({
  cors: { origin: '*' },
})
export class ChatGateway {
  constructor(private authService: AuthService) {}

  async handleConnection(client: Socket) {
    try {
      const token = client.handshake.auth.token;
      const user = await this.authService.validateToken(token);
      client.data.user = user;
    } catch (error) {
      client.disconnect();
    }
  }

  @SubscribeMessage('message')
  handleMessage(client: Socket, payload: any) {
    const user = client.data.user;
    this.server.emit('message', {
      ...payload,
      user: user.name,
      timestamp: new Date(),
    });
  }
}
Diagram: WebSocket Flow
Client ⇄ WebSocket Connection ⇄ Gateway
    ↓                              ↓
  Events                      Business Logic
    ↓                              ↓
  Real-time Updates          Database/Service
Use Cases:
  • Chat applications
  • Live dashboards
  • Multiplayer games
  • Real-time notifications
  • Collaborative editing

10.4 Event Sourcing

Event sourcing stores state as a sequence of events. Useful for audit logs, undo/redo, and complex business flows.

Event Store

// events/event-store.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class EventStoreService {
  private events: any[] = [];

  async append(streamId: string, event: any) {
    this.events.push({
      streamId,
      event,
      timestamp: new Date(),
    });
  }

  async getEvents(streamId: string) {
    return this.events.filter(e => e.streamId === streamId);
  }

  async replay(streamId: string) {
    const events = await this.getEvents(streamId);
    // Rebuild state from events
    return events.reduce((state, event) => {
      return this.applyEvent(state, event);
    }, {});
  }

  private applyEvent(state: any, event: any) {
    // Apply event to state
    return state;
  }
}

Event Handler

// users/events/handlers/user-created.handler.ts
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
import { UserCreatedEvent } from '../user-created.event';

@EventsHandler(UserCreatedEvent)
export class UserCreatedHandler implements IEventHandler<UserCreatedEvent> {
  constructor(private eventStore: EventStoreService) {}

  async handle(event: UserCreatedEvent) {
    await this.eventStore.append('users', event);
    // Update read model
    // Send notifications
  }
}
Pattern:
  • Store every change as an event
  • Rebuild state by replaying events
  • Separate write model (events) from read model (projections)
Tip: Use event sourcing for systems where you need a full history of changes, auditability, or the ability to replay events.

10.5 Real-World Case Study: Scalable Chat App

Let’s see how these patterns work together in a real-world scenario.

Architecture

Features:
  • REST API for user management
  • WebSocket gateway for real-time messaging
  • CQRS for separating chat commands and queries
  • Event sourcing for message history
  • GraphQL for flexible data queries

Implementation

// chat/chat.module.ts
import { Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import { ChatGateway } from './chat.gateway';
import { ChatController } from './chat.controller';
import { SendMessageHandler } from './commands/send-message.handler';
import { GetMessagesHandler } from './queries/get-messages.handler';

@Module({
  imports: [CqrsModule],
  controllers: [ChatController],
  providers: [
    ChatGateway,
    SendMessageHandler,
    GetMessagesHandler,
  ],
})
export class ChatModule {}
Diagram:
Client → REST API (NestJS) → CQRS Command → Event Store

  WebSocket Gateway ← Event Stream

  GraphQL Resolver → Read Model

10.6 Best Practices

Following best practices ensures your advanced patterns are used effectively.

When to Use Each Pattern

CQRS:
  • Complex domains with high read/write separation
  • Need to optimize reads and writes independently
  • Complex business logic
GraphQL:
  • Multiple frontend clients with different data needs
  • Need flexible queries
  • Real-time subscriptions required
WebSockets:
  • Real-time bidirectional communication
  • Live updates needed
  • Low latency requirements
Event Sourcing:
  • Need full audit trail
  • Complex business flows
  • Time travel/debugging needed

Don’t Over-Engineer

  • Start simple
  • Add complexity only when needed
  • Use patterns that solve real problems
  • Avoid premature optimization

Keep Code Modular

  • Separate concerns
  • Use dependency injection
  • Write tests
  • Document architecture

Monitor Performance

  • Profile advanced features
  • Monitor resource usage
  • Set up alerts
  • Optimize bottlenecks

10.7 Common Pitfalls

Avoid these common mistakes when using advanced patterns.

Overengineering

Don’t use CQRS, event sourcing, or GraphQL unless your use case truly needs them. Simple CRUD might be sufficient.

Poor Separation

Mixing command and query logic in CQRS defeats the purpose. Keep them separate.

Missing Documentation

Not documenting message/event formats leads to confusion between teams. Always document your contracts.

Security Oversights

Forgetting to secure WebSocket endpoints or GraphQL resolvers. Always implement authentication and authorization.

Performance Issues

Not monitoring or profiling advanced features can cause hidden performance issues. Always measure and optimize.

10.8 Summary

You’ve mastered advanced patterns in NestJS: Key Concepts:
  • CQRS: Separate read and write operations
  • GraphQL: Flexible query language for APIs
  • WebSockets: Real-time bidirectional communication
  • Event Sourcing: Store state as sequence of events
Best Practices:
  • Use advanced patterns only when needed
  • Keep code modular and testable
  • Document architecture decisions
  • Monitor and profile performance
  • Start simple, add complexity as needed
When to Use:
  • CQRS: Complex domains with read/write separation
  • GraphQL: Multiple clients with different data needs
  • WebSockets: Real-time communication required
  • Event Sourcing: Need full audit trail
With these tools, you can build modern, scalable applications with confidence!