Skip to content
NestJS ns websockets 5 min read

WebSocket Guards, Pipes & Filters

The same enhancers you bind to HTTP controllers — guards, pipes, and exception filters — also work inside WebSocket gateways, but with a different transport context and a different exception type. Guards decide whether a socket may invoke a handler, pipes transform and validate the incoming payload, and filters translate thrown errors into structured messages the client can read. This page shows how to reuse those building blocks in the WS world, validate message DTOs, throw and catch WsException, and authenticate a connecting socket from its handshake.

How the WS execution context differs

Enhancers receive an ExecutionContext, but for WebSocket handlers the underlying arguments are not (request, response) — they are (client, data). To read them in a transport-agnostic guard or interceptor you switch to the WS context first.

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

@Injectable()
export class WsThrottleGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const client: Socket = context.switchToWs().getClient<Socket>();
    const data = context.switchToWs().getData<unknown>();
    return Boolean(client) && data !== undefined;
  }
}

switchToWs() exposes getClient() (the socket) and getData() (the message payload). Calling switchToHttp() inside a gateway would yield undefined, so always branch on the transport when an enhancer is shared across both.

Validating payloads with pipes

Pipes run before the handler and can both transform and validate the @MessageBody() argument. The built-in ValidationPipe works exactly as it does for HTTP: pair it with a class-validator DTO and Nest rejects malformed messages automatically. Bind it per-handler with @UsePipes, or per-parameter for finer control.

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

export class SendMessageDto {
  @IsString()
  @MinLength(1)
  room: string;

  @IsString()
  @MaxLength(500)
  text: string;
}
import {
  WebSocketGateway,
  SubscribeMessage,
  MessageBody,
} from '@nestjs/websockets';
import { UsePipes, ValidationPipe } from '@nestjs/common';
import { SendMessageDto } from './dto/send-message.dto';

@WebSocketGateway({ cors: { origin: '*' } })
export class ChatGateway {
  @UsePipes(new ValidationPipe({ whitelist: true, transform: true }))
  @SubscribeMessage('message')
  handleMessage(@MessageBody() body: SendMessageDto): SendMessageDto {
    return { room: body.room, text: body.text.trim() };
  }
}

When validation fails, the pipe throws — and because we are in a WS context, Nest converts it into a WsException rather than an HTTP error.

Pipes need the runtime metadata class-validator relies on. Enable emitDecoratorMetadata and experimentalDecorators in tsconfig.json, and call app.useGlobalPipes(new ValidationPipe()) in main.ts if you want validation applied to every gateway without repeating @UsePipes.

Throwing and catching WsException

HTTP handlers throw HttpException; WebSocket handlers throw WsException. It is a lightweight error that carries either a string or an object payload. The default WS exception filter serializes it into an exception event sent back to the offending client.

import { SubscribeMessage, WebSocketGateway, MessageBody } from '@nestjs/websockets';
import { WsException } from '@nestjs/websockets';

@WebSocketGateway()
export class ChatGateway {
  @SubscribeMessage('message')
  handleMessage(@MessageBody() body: { text: string }) {
    if (body.text.length > 500) {
      throw new WsException('Message too long');
    }
    return { ok: true };
  }
}

Output:

client → emit('message', { text: '...600 chars...' })
client ← exception { status: 'error', message: 'Message too long' }

Customizing errors with a filter

To control the shape of that exception event — adding an error code, timestamp, or the original event name — write a filter that extends BaseWsExceptionFilter. Catching WsException lets you handle validation and business errors uniformly, while still delegating unknown errors to the base implementation.

import { ArgumentsHost, Catch } 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 message = typeof error === 'string' ? error : (error as any).message;

    client.emit('exception', {
      status: 'error',
      code: 'WS_ERROR',
      message,
      timestamp: new Date().toISOString(),
    });
  }
}

Bind it with @UseFilters(new WsExceptionFilter()) on a handler or gateway, or globally via app.useGlobalFilters(new WsExceptionFilter()).

Authenticating the socket with a guard

Sockets authenticate once, typically from a token passed in the handshake (auth payload or query string). A guard reads that token, verifies it, and attaches the user to the socket so later handlers can trust it. Throw a WsException to reject unauthorized messages.

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

@Injectable()
export class WsAuthGuard implements CanActivate {
  constructor(private readonly jwt: JwtService) {}

  canActivate(context: ExecutionContext): boolean {
    const client: Socket = context.switchToWs().getClient<Socket>();
    const token =
      client.handshake.auth?.token ??
      (client.handshake.query?.token as string | undefined);

    if (!token) {
      throw new WsException('Missing auth token');
    }

    try {
      const payload = this.jwt.verify(token);
      client.data.user = payload;
      return true;
    } catch {
      throw new WsException('Invalid auth token');
    }
  }
}
import { UseGuards } from '@nestjs/common';
import { SubscribeMessage, WebSocketGateway } from '@nestjs/websockets';
import { WsAuthGuard } from './ws-auth.guard';

@UseGuards(WsAuthGuard)
@WebSocketGateway({ cors: { origin: '*' } })
export class ChatGateway {
  @SubscribeMessage('message')
  handleMessage() {
    return { ok: true };
  }
}

Storing the user on client.data makes it available to every subsequent handler without re-verifying the token.

Enhancer reference

EnhancerInterface / baseWhen it runsReads context via
GuardCanActivateBefore the handlerswitchToWs().getClient()
PipePipeTransformOn each @MessageBody() argThe value + ArgumentMetadata
FilterBaseWsExceptionFilterOn a thrown exceptionswitchToWs().getClient()
InterceptorNestInterceptorAround the handlerswitchToWs().getData()

Guards run once per message, not per connection. For per-connection checks (rejecting a socket before any message), authenticate inside the gateway’s handleConnection lifecycle hook and call client.disconnect().

Best Practices

  • Always call context.switchToWs() (never switchToHttp()) inside WS enhancers, and guard against shared enhancers being reused across both transports.
  • Throw WsException — not HttpException — from gateway code so the WS exception layer can serialize it correctly.
  • Apply a global ValidationPipe with whitelist: true so every gateway rejects unknown or malformed fields by default.
  • Verify the token in a guard and stash the decoded user on client.data so downstream handlers stay stateless and trusted.
  • Extend BaseWsExceptionFilter rather than reimplementing it, so unexpected errors still get sane default handling.
  • Reject unauthenticated sockets in handleConnection for connection-level security, and use guards for per-message authorization.
Last updated June 14, 2026
Was this helpful?