Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

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 a library. The checkout desk (command side) handles lending books, accepting returns, and processing fines — these are write operations that change the state of the library. The catalog computer (query side) lets visitors search for books, check availability, and browse by genre — these are read operations that never change anything. In a small library, one librarian does both jobs. In a busy city library, you have separate staff and separate systems for each, because the checkout desk needs to be fast and transactional, while the catalog needs to be optimized for search. That is exactly why CQRS exists: when your read and write workloads have fundamentally different performance characteristics, separating them lets you optimize each independently.

Installation

npm install @nestjs/cqrs

Command Example

Commands are simple data classes — they carry the intent (“create this user”) and the data needed to fulfill it. The handler is where the actual business logic lives. This separation makes it trivial to add cross-cutting concerns (logging, validation, event publishing) to all commands via decorators or middleware.
// users/commands/create-user.command.ts
// Commands are named as imperative verbs: "Create", "Update", "Delete".
// They are plain classes with no behavior -- just data.
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 binds this handler to CreateUserCommand.
// When commandBus.execute(new CreateUserCommand(...)) is called,
// NestJS routes it to this handler's execute() method.
@CommandHandler(CreateUserCommand)
export class CreateUserHandler implements ICommandHandler<CreateUserCommand> {
  constructor(private usersService: UsersService) {}

  // The execute method contains the business logic for this command.
  // It can do validation, call services, publish events, etc.
  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 — the write model can be normalized for data integrity, while the read model can be denormalized for query performance
  • Implement event sourcing for auditability when you need a full history of changes
  • Use CQRS for complex domains, not simple CRUD — adding CQRS to a basic user management API is overengineering that will slow your team down with no benefit
  • Keep commands and queries focused — each should do exactly one thing
When NOT to Use CQRS: If your application is primarily CRUD with simple business rules, CQRS adds layers of indirection (command classes, handler classes, bus routing) that make the code harder to follow without providing proportional benefit. As a rule of thumb, if your service methods are fewer than 20 lines and rarely need to coordinate multiple operations, stick with the standard controller-service-repository pattern.

10.2 GraphQL APIs

NestJS supports GraphQL out of the box with two approaches: code-first (define your schema using TypeScript decorators — the schema.gql file is auto-generated) and schema-first (write the .graphql schema file manually, then generate TypeScript types). Code-first is more popular in the NestJS ecosystem because it keeps your types and schema in sync automatically. GraphQL lets clients request exactly the fields they need — no more over-fetching 50 user fields when you only need name and email.

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 Advanced Pattern Comparison

PatternComplexityFiles Added (per feature)When It Pays OffWhen It Hurts
Standard RESTLow3-4 (controller, service, dto, module)CRUD apps, prototypes, small teamsNever — this is the baseline
CQRSMedium-High8-12 (commands, queries, handlers, events)Read/write asymmetry, complex domainsSimple CRUD (adds indirection for no benefit)
GraphQLMedium5-7 (resolver, input types, object types)Multiple frontends, mobile + webInternal APIs with one consumer
WebSocketsMedium3-5 (gateway, events, room logic)Real-time features (chat, dashboards)Request-response APIs
Event SourcingHigh10-15 (event store, projections, replay)Audit trails, financial systems, undo/redoMost CRUD applications
CQRS + Event SourcingVery High15-20Domain with complex state transitions, regulatory audit requirementsEverything else

REST vs GraphQL Decision Framework

FactorChoose RESTChoose GraphQL
Number of clients1-2 (web + maybe mobile, same data needs)3+ (web, mobile, partner API, each needing different fields)
Data shapePredictable, well-defined resourcesHighly variable, nested relationships
CachingEasy (HTTP caching, CDN, ETags)Hard (query-level caching requires tools like Apollo Cache)
File uploadsNative (multipart/form-data)Awkward (requires separate endpoint or complex spec)
Real-timeRequires separate WebSocket setupBuilt-in subscriptions
ToolingPostman, curl, any HTTP clientGraphQL Playground, Apollo DevTools
Team familiarityUniversalRequires GraphQL-specific knowledge
API evolutionVersioning (v1/v2) or content negotiationField deprecation (no versioning needed)
Practical Advice: You do not have to choose one. Many production NestJS applications use REST for public APIs (easy to cache, familiar to consumers) and GraphQL for internal frontends (flexible queries, no over-fetching). NestJS supports both in the same application simultaneously.

When to Add WebSockets vs Polling

FactorUse WebSocketsUse Polling / SSE
Update frequencyMultiple times per secondEvery few seconds or less
Bidirectional?Client and server both send dataServer pushes only
Connection countManageable (hundreds to low thousands)Very high (tens of thousands — SSE is lighter)
InfrastructureRequires sticky sessions or Redis adapterStateless, works with any load balancer
ComplexityHigher (connection management, reconnection)Lower (HTTP-based, automatic reconnection)

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

This is by far the most common pitfall. CQRS, event sourcing, and GraphQL are powerful tools, but they come with real costs: more files, more indirection, steeper learning curves for new team members. If your team spends more time maintaining the infrastructure of CQRS than writing business logic, you over-engineered. Start with the simplest architecture that works and add complexity only when you feel real pain.

Poor Separation

Mixing command and query logic in CQRS defeats the purpose. A command handler that also queries the database to return data is not CQRS — it is a regular service with extra steps. Commands should return at most an ID or acknowledgment; the client should use a query to fetch the created resource.

Missing Documentation

Not documenting message/event formats leads to confusion between teams. When Service A publishes a user_created event, Service B needs to know the exact payload shape. Use shared DTOs or schema registries to keep contracts in sync. This is especially critical because these contracts are not enforced by the compiler — a missing field will cause a runtime error, not a build error.

Security Oversights

WebSocket endpoints and GraphQL resolvers need authentication just like REST endpoints. It is easy to forget because they feel “different” from traditional HTTP routes. Always apply JWT guards to WebSocket gateways (in handleConnection) and to GraphQL resolvers (using @UseGuards). GraphQL is particularly risky because a single unprotected resolver can expose your entire data model through introspection.

Performance Issues

GraphQL’s flexibility is a double-edged sword. Clients can write deeply nested queries that join 10 tables, and your server will happily try to resolve them. Use query depth limiting (depthLimit), query complexity analysis, and DataLoader to prevent N+1 query explosions. Always profile before and after adding advanced patterns — measure, do not guess.

Edge Cases in Advanced Patterns

Edge Case 1: CQRS command returns data — is that allowed? Purist CQRS says commands should return void (or at most an ID), because commands change state and queries read state — mixing them violates the separation. In practice, returning the created entity’s ID from a command handler is fine and avoids an extra round-trip query. What you should not do is return a fully populated entity from a command handler, because that couples the write model to the read model. Edge Case 2: GraphQL N+1 with nested resolvers If a User type has a posts field resolver, and you query { users { posts { title } } } for 100 users, the posts resolver fires 100 times — one database query per user. DataLoader batches these into a single query. Without DataLoader, this pattern destroys database performance.
// Without DataLoader: 100 users = 1 query for users + 100 queries for posts = 101 queries
// With DataLoader:    100 users = 1 query for users + 1 query for all posts  = 2 queries

import DataLoader from 'dataloader';

// Create a DataLoader that batches user IDs into a single posts query
const postsLoader = new DataLoader(async (userIds: number[]) => {
  const posts = await this.postsService.findByUserIds(userIds);
  // DataLoader requires results in the same order as input IDs
  return userIds.map(id => posts.filter(p => p.authorId === id));
});
Edge Case 3: WebSocket scaling across multiple server instances If you run 3 NestJS instances behind a load balancer, a WebSocket connection is bound to one instance. When Instance A emits an event, clients connected to Instances B and C do not receive it. Use the Redis adapter for Socket.io to broadcast events across all instances:
import { IoAdapter } from '@nestjs/platform-socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';

export class RedisIoAdapter extends IoAdapter {
  createIOServer(port: number, options?: any) {
    const server = super.createIOServer(port, options);
    const pubClient = createClient({ url: 'redis://localhost:6379' });
    const subClient = pubClient.duplicate();
    server.adapter(createAdapter(pubClient, subClient));
    return server;
  }
}
Edge Case 4: Event sourcing and eventual consistency When you separate the write model (events) from the read model (projections), the read model is always slightly behind. If a user creates a resource and immediately queries for it, the projection may not have processed the event yet, returning “not found.” Solutions: (1) Return the created entity directly from the command handler (breaks strict CQRS but is pragmatic); (2) Use a synchronous event handler that updates the projection before the command returns; (3) Accept eventual consistency and handle it in the frontend (show optimistic UI).

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!