tests.ws

NestJS WebSocket Guide

nestjs websocket typescript gateway socket-io

NestJS WebSocket support provides a structured, decorator-based approach to building real-time applications. The framework abstracts Socket.IO and ws libraries behind a unified gateway interface, allowing developers to handle bidirectional communication with the same patterns used for HTTP controllers. Understanding how to implement a nestjs websocket server means leveraging decorators, dependency injection, and middleware to create maintainable WebSocket applications that integrate seamlessly with the rest of your NestJS architecture.

The gateway pattern in NestJS treats WebSocket connections as first-class citizens alongside HTTP routes. You define message handlers using decorators, apply guards and pipes for validation, and use the same exception handling mechanisms. This consistency reduces cognitive overhead when building full-stack applications that require both REST APIs and real-time features. Whether you need chat functionality, live notifications, or collaborative editing, NestJS provides the tools to implement these features without abandoning the framework’s conventions.

Installation and Setup

To add WebSocket support to your NestJS project, install the core WebSocket package and a platform adapter. The most common choice is Socket.IO due to its automatic reconnection, room support, and fallback transports.

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

The @nestjs/websockets package provides the gateway decorators and base classes, while @nestjs/platform-socket.io implements the Socket.IO adapter. If you prefer the lighter ws library, you can install @nestjs/platform-ws instead. The gateway code remains nearly identical regardless of which adapter you choose.

For a complete understanding of WebSocket fundamentals, review the protocol specifications before implementing production gateways. NestJS abstracts many details, but knowing the underlying protocol helps when debugging connection issues or optimizing performance.

Creating a WebSocket Gateway

A gateway in NestJS is a class annotated with @WebSocketGateway() that handles incoming WebSocket connections and messages. Create a gateway by generating a new resource or manually creating the class.

import {
  WebSocketGateway,
  WebSocketServer,
  SubscribeMessage,
  MessageBody,
  ConnectedSocket,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';

@WebSocketGateway({
  cors: {
    origin: '*',
  },
})
export class ChatGateway {
  @WebSocketServer()
  server: Server;

  @SubscribeMessage('message')
  handleMessage(
    @MessageBody() data: string,
    @ConnectedSocket() client: Socket,
  ): string {
    return `Echo: ${data}`;
  }

  afterInit(server: Server) {
    console.log('WebSocket gateway initialized');
  }

  handleConnection(client: Socket) {
    console.log(`Client connected: ${client.id}`);
  }

  handleDisconnect(client: Socket) {
    console.log(`Client disconnected: ${client.id}`);
  }
}

The @WebSocketGateway() decorator accepts configuration options including port, namespace, and CORS settings. If you omit the port, the gateway runs on the same HTTP server as your REST API. The @WebSocketServer() decorator injects the Socket.IO server instance, giving you access to broadcast methods and connection management.

Lifecycle hooks like afterInit, handleConnection, and handleDisconnect provide entry points for initialization logic, authentication, and cleanup. These methods execute automatically at the appropriate times during the gateway lifecycle.

Register the gateway in your module’s providers array:

import { Module } from '@nestjs/common';
import { ChatGateway } from './chat.gateway';

@Module({
  providers: [ChatGateway],
})
export class ChatModule {}

NestJS instantiates the gateway when the module loads, starting the WebSocket server and registering message handlers.

WebSocket Decorators

The @SubscribeMessage() decorator maps incoming event names to handler methods. When a client emits an event, NestJS routes it to the corresponding handler based on the event name.

@SubscribeMessage('chat:message')
handleChatMessage(
  @MessageBody() message: CreateMessageDto,
  @ConnectedSocket() client: Socket,
): WsResponse<string> {
  this.server.emit('chat:message', {
    id: Date.now(),
    text: message.text,
    userId: client.data.userId,
    timestamp: new Date(),
  });

  return {
    event: 'chat:message:sent',
    data: 'Message delivered',
  };
}

The @MessageBody() decorator extracts the message payload, while @ConnectedSocket() provides access to the client socket. You can return a plain value, an observable, or a WsResponse object that specifies both the event name and data.

For extracting specific fields from nested message structures, use parameter decorators with property paths:

@SubscribeMessage('user:update')
handleUserUpdate(
  @MessageBody('userId') userId: string,
  @MessageBody('profile') profile: UpdateProfileDto,
) {
  // userId and profile extracted from message payload
}

The @ConnectedSocket() decorator gives you the raw Socket.IO client instance, allowing you to access connection metadata, emit events directly to that client, or join rooms.

Working with Rooms and Broadcasting

Rooms in Socket.IO group connections for targeted message delivery. A nestjs socket can join multiple rooms, and you can broadcast messages to all sockets in a specific room without iterating through connections manually.

@WebSocketGateway()
export class GameGateway {
  @WebSocketServer()
  server: Server;

  @SubscribeMessage('game:join')
  handleJoinGame(
    @MessageBody('gameId') gameId: string,
    @ConnectedSocket() client: Socket,
  ) {
    client.join(`game:${gameId}`);
    client.data.gameId = gameId;

    this.server.to(`game:${gameId}`).emit('player:joined', {
      playerId: client.id,
      playerCount: this.server.sockets.adapter.rooms.get(`game:${gameId}`)?.size,
    });

    return { event: 'game:joined', data: { gameId } };
  }

  @SubscribeMessage('game:move')
  handleGameMove(
    @MessageBody() move: GameMoveDto,
    @ConnectedSocket() client: Socket,
  ) {
    const gameId = client.data.gameId;
    if (!gameId) {
      throw new WsException('Not in a game');
    }

    // Broadcast to all players in the game except sender
    client.to(`game:${gameId}`).emit('game:move', {
      playerId: client.id,
      move,
    });
  }

  handleDisconnect(client: Socket) {
    if (client.data.gameId) {
      this.server.to(`game:${client.data.gameId}`).emit('player:left', {
        playerId: client.id,
      });
    }
  }
}

The client.join() method adds the socket to a room. Use this.server.to(roomName) to broadcast to all sockets in that room, or client.to(roomName) to broadcast to all sockets in the room except the sender. Store room identifiers in client.data for easy access in subsequent message handlers.

To broadcast to all connected clients regardless of rooms, use this.server.emit(). For more targeted messaging, combine rooms with Socket.IO’s namespace feature by configuring the gateway with a namespace option:

@WebSocketGateway({ namespace: 'chat' })
export class ChatGateway {
  // All connections to /chat namespace
}

Using the ws Adapter

While Socket.IO is the default, you can switch to the lighter ws library for applications that don’t need automatic reconnection or room management. Install the ws platform adapter:

npm install @nestjs/platform-ws ws
npm install -D @types/ws

Update your gateway to use ws-specific types:

import {
  WebSocketGateway,
  WebSocketServer,
  SubscribeMessage,
  MessageBody,
  ConnectedSocket,
} from '@nestjs/websockets';
import { Server } from 'ws';
import * as WebSocket from 'ws';

@WebSocketGateway({ transports: ['websocket'] })
export class WsGateway {
  @WebSocketServer()
  server: Server;

  @SubscribeMessage('message')
  handleMessage(
    @MessageBody() data: any,
    @ConnectedSocket() client: WebSocket,
  ) {
    return { event: 'message', data: `Received: ${data}` };
  }

  handleConnection(client: WebSocket) {
    console.log('Client connected');
  }
}

Configure the application to use the ws adapter in your main.ts file:

import { NestFactory } from '@nestjs/core';
import { WsAdapter } from '@nestjs/platform-ws';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useWebSocketAdapter(new WsAdapter(app));
  await app.listen(3000);
}
bootstrap();

The ws library provides a lower-level WebSocket implementation without the extras that Socket.IO includes. This results in smaller bundle sizes and less overhead, but you lose features like automatic reconnection, rooms, and namespace support. For more details on the ws library, see the Node.js ws guide. For Socket.IO specifics, review the Socket.IO guide.

Guards and Pipes with WebSockets

NestJS guards and pipes work with WebSocket gateways the same way they work with HTTP controllers. Apply guards to verify authentication, and use pipes to validate and transform incoming message payloads.

Create a WebSocket guard by implementing the CanActivate interface:

import { CanActivate, Injectable, ExecutionContext } from '@nestjs/common';
import { WsException } from '@nestjs/websockets';
import { Socket } from 'socket.io';

@Injectable()
export class WsAuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const client: Socket = context.switchToWs().getClient();
    const token = client.handshake.auth.token;

    if (!token || !this.validateToken(token)) {
      throw new WsException('Unauthorized');
    }

    client.data.userId = this.extractUserId(token);
    return true;
  }

  private validateToken(token: string): boolean {
    // Verify JWT or session token
    return token.startsWith('valid-');
  }

  private extractUserId(token: string): string {
    // Extract user ID from token
    return token.split('-')[1];
  }
}

Apply the guard at the gateway level or on individual message handlers:

@WebSocketGateway()
@UseGuards(WsAuthGuard)
export class SecureGateway {
  @SubscribeMessage('secure:message')
  handleSecureMessage(
    @MessageBody() data: string,
    @ConnectedSocket() client: Socket,
  ) {
    const userId = client.data.userId;
    // Process authenticated message
  }
}

Use validation pipes to ensure message payloads match expected schemas:

import { IsString, IsNotEmpty, MaxLength } from 'class-validator';

export class CreateMessageDto {
  @IsString()
  @IsNotEmpty()
  @MaxLength(500)
  text: string;

  @IsString()
  @IsNotEmpty()
  channelId: string;
}

@SubscribeMessage('message:create')
@UsePipes(new ValidationPipe())
handleCreateMessage(
  @MessageBody() createMessageDto: CreateMessageDto,
  @ConnectedSocket() client: Socket,
) {
  // createMessageDto is validated and transformed
}

The ValidationPipe automatically validates the DTO and throws a WsException if validation fails. The exception gets caught by NestJS and emitted back to the client as an error event.

Exception Handling

WebSocket exceptions in NestJS use the WsException class, which formats errors for transmission over WebSocket connections. Throw WsException from message handlers or guards to send error messages to clients.

import { WsException } from '@nestjs/websockets';

@SubscribeMessage('room:join')
handleJoinRoom(
  @MessageBody('roomId') roomId: string,
  @ConnectedSocket() client: Socket,
) {
  if (!this.roomExists(roomId)) {
    throw new WsException('Room not found');
  }

  if (this.isRoomFull(roomId)) {
    throw new WsException({
      message: 'Room is full',
      code: 'ROOM_FULL',
      maxCapacity: 10,
    });
  }

  client.join(roomId);
}

Create a WebSocket exception filter for custom error handling:

import { Catch, ArgumentsHost } from '@nestjs/common';
import { BaseWsExceptionFilter, WsException } from '@nestjs/websockets';
import { Socket } from 'socket.io';

@Catch(WsException)
export class WsExceptionFilter extends BaseWsExceptionFilter {
  catch(exception: WsException, host: ArgumentsHost) {
    const client = host.switchToWs().getClient<Socket>();
    const error = exception.getError();
    const details = typeof error === 'string' ? { message: error } : error;

    client.emit('error', {
      timestamp: new Date().toISOString(),
      ...details,
    });
  }
}

Apply the filter to your gateway:

@WebSocketGateway()
@UseFilters(new WsExceptionFilter())
export class ChatGateway {
  // Message handlers
}

Exception filters intercept thrown exceptions before they reach the client, allowing you to log errors, transform error messages, or emit errors on specific event names.

Testing WebSocket Gateways

Test NestJS gateways by creating a testing module and mocking Socket.IO clients. Use the NestJS testing utilities to instantiate the gateway with its dependencies.

import { Test, TestingModule } from '@nestjs/testing';
import { ChatGateway } from './chat.gateway';
import { Server, Socket } from 'socket.io';

describe('ChatGateway', () => {
  let gateway: ChatGateway;
  let server: Server;
  let mockClient: Partial<Socket>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [ChatGateway],
    }).compile();

    gateway = module.get<ChatGateway>(ChatGateway);

    mockClient = {
      id: 'test-client-id',
      data: {},
      join: jest.fn(),
      to: jest.fn().mockReturnThis(),
      emit: jest.fn(),
    };

    server = {
      emit: jest.fn(),
      to: jest.fn().mockReturnThis(),
      sockets: {
        adapter: {
          rooms: new Map(),
        },
      },
    } as any;

    gateway.server = server;
  });

  it('should handle message events', () => {
    const result = gateway.handleMessage('test message', mockClient as Socket);
    expect(result).toBe('Echo: test message');
  });

  it('should join game room', () => {
    gateway.handleJoinGame('game-123', mockClient as Socket);
    expect(mockClient.join).toHaveBeenCalledWith('game:game-123');
    expect(mockClient.data.gameId).toBe('game-123');
  });

  it('should broadcast to room on game move', () => {
    mockClient.data.gameId = 'game-123';
    const move = { x: 5, y: 10 };

    gateway.handleGameMove(move as any, mockClient as Socket);
    expect(mockClient.to).toHaveBeenCalledWith('game:game-123');
  });

  it('should throw exception for invalid room', () => {
    expect(() => {
      gateway.handleJoinRoom('invalid-room', mockClient as Socket);
    }).toThrow(WsException);
  });
});

For integration tests, create a real Socket.IO server and client:

import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { io, Socket as ClientSocket } from 'socket.io-client';
import { AppModule } from '../src/app.module';

describe('ChatGateway (e2e)', () => {
  let app: INestApplication;
  let client: ClientSocket;

  beforeAll(async () => {
    const moduleRef = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleRef.createNestApplication();
    await app.listen(3001);

    client = io('http://localhost:3001', {
      transports: ['websocket'],
    });

    await new Promise<void>((resolve) => {
      client.on('connect', () => resolve());
    });
  });

  afterAll(async () => {
    client.disconnect();
    await app.close();
  });

  it('should receive message response', (done) => {
    client.emit('message', 'test');
    client.on('message', (data) => {
      expect(data).toContain('Echo: test');
      done();
    });
  });

  it('should handle room broadcasts', (done) => {
    const client2 = io('http://localhost:3001', {
      transports: ['websocket'],
    });

    client2.on('connect', () => {
      client.emit('game:join', { gameId: 'test-game' });
      client2.emit('game:join', { gameId: 'test-game' });
    });

    client2.on('player:joined', (data) => {
      expect(data.playerCount).toBeGreaterThan(0);
      client2.disconnect();
      done();
    });
  });
});

For comprehensive testing strategies, see how to test WebSockets for patterns that apply beyond NestJS-specific implementations.

Advanced Gateway Patterns

Combine multiple service dependencies in your gateway to build complex real-time features. Inject repositories, services, and external clients using NestJS dependency injection.

@WebSocketGateway()
export class NotificationGateway {
  constructor(
    private readonly userService: UserService,
    private readonly notificationService: NotificationService,
  ) {}

  @WebSocketServer()
  server: Server;

  async handleConnection(client: Socket) {
    const userId = await this.authenticateConnection(client);
    if (!userId) {
      client.disconnect();
      return;
    }

    client.data.userId = userId;
    client.join(`user:${userId}`);

    const unreadCount = await this.notificationService.getUnreadCount(userId);
    client.emit('notification:count', { unread: unreadCount });
  }

  @SubscribeMessage('notification:markRead')
  async handleMarkRead(
    @MessageBody('notificationId') notificationId: string,
    @ConnectedSocket() client: Socket,
  ) {
    await this.notificationService.markAsRead(
      notificationId,
      client.data.userId,
    );

    const newCount = await this.notificationService.getUnreadCount(
      client.data.userId,
    );

    client.emit('notification:count', { unread: newCount });
  }

  async sendToUser(userId: string, event: string, data: any) {
    this.server.to(`user:${userId}`).emit(event, data);
  }

  private async authenticateConnection(client: Socket): Promise<string | null> {
    const token = client.handshake.auth.token;
    return this.userService.validateToken(token);
  }
}

This pattern allows other parts of your application to send real-time notifications by injecting the gateway and calling sendToUser(). The gateway acts as a bridge between your business logic and connected clients.

For namespacing different features, create multiple gateways with distinct namespaces:

@WebSocketGateway({ namespace: 'chat' })
export class ChatGateway {
  // Chat-specific handlers
}

@WebSocketGateway({ namespace: 'notifications' })
export class NotificationGateway {
  // Notification-specific handlers
}

Clients connect to specific namespaces using the path in the connection URL: io('http://localhost:3000/chat') and io('http://localhost:3000/notifications').

Frequently Asked Questions

How do I authenticate WebSocket connections in NestJS?

Extract authentication tokens from the connection handshake and validate them in a guard or in the handleConnection lifecycle hook. Store user information in client.data for access in message handlers.

handleConnection(client: Socket) {
  const token = client.handshake.auth.token || client.handshake.headers.authorization;
  const user = this.authService.validateToken(token);

  if (!user) {
    client.disconnect();
    return;
  }

  client.data.user = user;
  client.join(`user:${user.id}`);
}

Disconnect unauthorized clients immediately to prevent them from sending messages.

Can I use the same port for HTTP and WebSocket in NestJS?

Yes, NestJS runs the WebSocket server on the same HTTP server by default if you don’t specify a port in the @WebSocketGateway() decorator. This allows both REST endpoints and WebSocket connections on a single port.

@WebSocketGateway() // Uses same port as HTTP server
export class AppGateway {}

To run on a different port, specify it in the decorator:

@WebSocketGateway(8080) // Runs on port 8080
export class AppGateway {}

How do I handle WebSocket reconnection in NestJS?

Socket.IO handles reconnection automatically on the client side. On the server, treat each connection as potentially new. Store session state in a database or cache rather than in memory, and restore state when clients reconnect.

handleConnection(client: Socket) {
  const sessionId = client.handshake.auth.sessionId;
  if (sessionId) {
    const session = await this.sessionService.restore(sessionId);
    if (session) {
      client.data.session = session;
      client.join(session.roomId);
    }
  }
}

For the ws adapter, implement reconnection logic on the client since ws doesn’t include automatic reconnection.

What is the difference between Socket.IO and ws adapters in NestJS?

Socket.IO provides automatic reconnection, rooms, namespaces, and fallback transports, making it suitable for production applications where reliability matters. The ws adapter is lighter and faster but requires manual implementation of these features. Use Socket.IO for full-featured applications and ws when you need minimal overhead and can handle reconnection logic yourself. Both adapters work with the same gateway code, making it easy to switch between them by changing the adapter configuration in your main.ts file.