Gateways
A gateway is a class that answers WebSocket events instead of HTTP routes, with hooks for a connection's whole life.
@WebSocketGateway(port?: number, options?: GatewayMetadata)An HTTP request arrives, gets an answer, and the connection ends. A WebSocket connection stays open and either side can send a message at any moment. A gateway is the class Nest uses for that second style: a class decorated with @WebSocketGateway() whose methods react to named events rather than to URLs. Install @nestjs/websockets and @nestjs/platform-socket.io to get one.
A gateway is also a provider, which in Nest means a class the framework constructs once and keeps in its container, so constructor injection works exactly as it does in a service. It is only constructed if you list it in the providers array of a module, the class marked @Module() that tells Nest what to wire together.
Where things run
By default the gateway shares the HTTP server's port. When a client connects, the adapter completes the handshake, then handleConnection() runs. For every message after that the order is: guards, interceptors, pipes, your handler, interceptors again, exception filters. HTTP middleware is not in that list, so anything registered through configure() or app.use() never sees a WebSocket message. Global guards, pipes and interceptors do apply to gateways; global exception filters do not, so bind those with @UseFilters() on the gateway or the method.
Answering
Returning a value sends it as the socket.io acknowledgement to that one client. Returning { event, data } (the WsResponse interface) emits an event instead, and returning an Observable emits once per value. Returning nothing sends nothing.
Example
// src/events/events.gateway.ts
import { Logger } from '@nestjs/common';
import {
ConnectedSocket,
MessageBody,
OnGatewayConnection,
OnGatewayDisconnect,
OnGatewayInit,
SubscribeMessage,
WebSocketGateway,
WebSocketServer,
WsResponse,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { PresenceService } from './presence.service';
// No port argument, so this gateway listens on the HTTP server's port.
// The second argument is passed straight to the socket.io server constructor.
WebSocketGateway({ cors: { origin: 'https://app.example.com' } })
export class EventsGateway
implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
{
private readonly logger = new Logger(EventsGateway.name);
// Nest assigns the socket.io Server here once it exists. It is undefined
// in the constructor, so never touch it from there.
WebSocketServer()
private readonly server: Server;
// A gateway is a provider: ordinary constructor injection.
constructor(private readonly presence: PresenceService) {}
// OnGatewayInit: runs once, with the library-specific server instance.
afterInit(server: Server): void {
this.logger.log(`socket.io ready, path ${server.path()}`);
}
// OnGatewayConnection: runs after the handshake succeeds, per client.
handleConnection(client: Socket): void {
this.presence.add(client.id);
client.emit('welcome', { id: client.id, online: this.presence.count() });
}
// OnGatewayDisconnect. Since @nestjs/websockets 12 the typed signature
// carries socket.io's disconnect reason, e.g. 'transport close'.
handleDisconnect(client: Socket, reason?: string): void {
this.presence.remove(client.id);
this.logger.log(`${client.id} left: ${reason ?? 'unknown'}`);
}
// Maps the event name a client emits to this method.
SubscribeMessage('ping')
handlePing(
MessageBody() data: { at: number },
ConnectedSocket() client: Socket,
): WsResponse<{ at: number; id: string }> {
// Returning { event, data } emits an event instead of acknowledging,
// so the client listens with client.on('pong', ...).
return { event: 'pong', data: { at: data.at, id: client.id } };
}
}When to use it
- Push build or upload progress to a browser without the page polling an endpoint.
- Deliver a notification the moment a queue processor finishes a job.
- Stream price or sensor updates to a dashboard that must stay current.
- Let two users edit the same record and see each other's changes.
More examples
The same handler in longhand and shorthand
Four spellings of one handler. Reach for the positional form only when reading someone else's code, because unit tests then have to fabricate a socket for every call. The decorator form lets a test pass just the payload. @Ack() is for the cases where you need to acknowledge from inside a callback or after a branch, rather than at the end of the method.
// src/events/forms.gateway.ts
import {
Ack,
ConnectedSocket,
MessageBody,
SubscribeMessage,
WebSocketGateway,
} from '@nestjs/websockets';
import { Socket } from 'socket.io';
@WebSocketGateway()
export class FormsGateway {
// Longhand: positional parameters. The socket is always first, the payload
// second, and the acknowledgement callback third if the client sent one.
@SubscribeMessage('join:long')
joinLong(client: Socket, data: { room: string }) {
client.join(data.room);
return { ok: true };
}
// Shorthand: the same two values, named by decorators, in any order.
// The docs call these two forms functionally equivalent.
@SubscribeMessage('join:short')
joinShort(
@MessageBody() data: { room: string },
@ConnectedSocket() client: Socket,
) {
client.join(data.room);
return { ok: true }; // sent back as the acknowledgement payload
}
// @MessageBody('room') pulls one property out of the payload, the way
// @Body('room') does over HTTP.
@SubscribeMessage('join:field')
joinField(
@MessageBody('room') room: string,
@ConnectedSocket() client: Socket,
) {
client.join(room);
return { ok: true };
}
// @Ack() hands you the callback and switches off the automatic
// acknowledgement, so nothing is sent unless you call ack() yourself.
@SubscribeMessage('join:ack')
joinAck(
@MessageBody() data: { room: string },
@ConnectedSocket() client: Socket,
@Ack() ack: (response: { ok: boolean }) => void,
) {
client.join(data.room);
ack({ ok: true });
}
}Registering the gateway, and the error path
The module entry is the step people forget. The filter shows the WebSocket error path: a WsException becomes an 'exception' message of the shape { status: 'error', message, cause }, and anything else becomes the same shape with 'Internal server error'. Bind it with @UseFilters(AllWsExceptionsFilter) on the gateway class, because filters registered through app.useGlobalFilters() or APP_FILTER do not apply to gateways.
// src/events/events.module.ts
import { Module } from '@nestjs/common';
import { EventsGateway } from './events.gateway';
import { PresenceService } from './presence.service';
@Module({
// Drop EventsGateway from this array and the gateway is never built.
providers: [EventsGateway, PresenceService],
exports: [EventsGateway], // so other modules can inject it and emit
})
export class EventsModule {}
// src/common/all-ws-exceptions.filter.ts
import { ArgumentsHost, Catch, Logger } from '@nestjs/common';
import { BaseWsExceptionFilter } from '@nestjs/websockets';
@Catch() // no argument: catch everything thrown in a gateway
export class AllWsExceptionsFilter extends BaseWsExceptionFilter {
private readonly logger = new Logger(AllWsExceptionsFilter.name);
constructor() {
// includeCause: false stops the triggering payload being echoed back
// inside the error message. Default is true.
super({ includeCause: false });
}
catch(exception: unknown, host: ArgumentsHost) {
const ws = host.switchToWs();
this.logger.warn(`failed event: ${ws.getPattern()}`);
super.catch(exception, host); // emits the 'exception' message to the client
}
}Per-connection state with a request-scoped provider
Use this when a connection needs its own state and you would otherwise keep a Map keyed by socket id inside a singleton. Nest creates the instance when the socket connects and tears it down on disconnect, so there is no map to prune. It costs one instantiation per connection, so keep the default singleton scope unless you actually need the per-connection instance.
// src/events/connection-state.service.ts
import { Inject, Injectable, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Socket } from 'socket.io';
// Scope.REQUEST in a gateway means one instance per connected socket,
// alive for as long as that socket is. Supported from NestJS 12.
@Injectable({ scope: Scope.REQUEST })
export class ConnectionStateService {
private sequence = 0;
// The REQUEST token resolves to the socket here, not to an HTTP request.
constructor(@Inject(REQUEST) private readonly client: Socket) {}
next() {
return { clientId: this.client.id, sequence: ++this.sequence };
}
}
// src/events/sequence.gateway.ts
import { SubscribeMessage, WebSocketGateway } from '@nestjs/websockets';
import { ConnectionStateService } from './connection-state.service';
@WebSocketGateway()
export class SequenceGateway {
constructor(private readonly state: ConnectionStateService) {}
@SubscribeMessage('state')
onState() {
// Counts up across messages on one socket; a second client starts at 1.
return { event: 'state', data: this.state.next() };
}
}
Discussion