Gateways & Message Handlers
A gateway is the WebSocket equivalent of a controller: a class decorated with @WebSocketGateway() whose methods react to named events sent over a persistent socket connection. Instead of mapping HTTP verbs and routes, you map event names to handler methods with @SubscribeMessage, pull the payload out with @MessageBody, and reach the underlying socket with @ConnectedSocket. This page shows how to wire those decorators together to receive events and push responses back to one client or to every connected client.
Defining a gateway
A gateway is just an @Injectable-style class registered in a module’s providers array. The @WebSocketGateway decorator turns it into a socket endpoint and accepts the same options as the underlying platform (Socket.IO by default). Each event handler is an ordinary async method annotated with @SubscribeMessage('eventName').
import {
WebSocketGateway,
SubscribeMessage,
MessageBody,
} from '@nestjs/websockets';
@WebSocketGateway({ cors: { origin: '*' } })
export class ChatGateway {
@SubscribeMessage('ping')
handlePing(@MessageBody() data: string): string {
return `pong: ${data}`;
}
}
Register it like any provider so Nest’s DI container can construct it:
import { Module } from '@nestjs/common';
import { ChatGateway } from './chat.gateway';
@Module({
providers: [ChatGateway],
})
export class ChatModule {}
When a client emits ping with the value "hello", the value returned from the handler is sent back to that same client as an acknowledgement.
Output:
client → emit('ping', 'hello')
client ← ack: "pong: hello"
Reading the payload with @MessageBody
@MessageBody() injects the data the client sent with the event. Without arguments it gives you the whole payload; pass a property name to extract a single field. Combine it with a DTO interface to keep handlers type-safe.
import {
WebSocketGateway,
SubscribeMessage,
MessageBody,
} from '@nestjs/websockets';
interface ChatMessage {
room: string;
text: string;
}
@WebSocketGateway()
export class ChatGateway {
@SubscribeMessage('message')
handleMessage(@MessageBody() body: ChatMessage): ChatMessage {
return { room: body.room, text: body.text.trim() };
}
@SubscribeMessage('rename')
handleRename(@MessageBody('name') name: string): void {
console.log(`Renamed to ${name}`);
}
}
Accessing the socket with @ConnectedSocket
To do anything beyond returning a value — joining rooms, reading the client id, emitting to others — you need the socket instance itself. @ConnectedSocket() injects the connected client. With Socket.IO that is a Socket; you can declare both decorators in the same handler.
import {
WebSocketGateway,
SubscribeMessage,
MessageBody,
ConnectedSocket,
} from '@nestjs/websockets';
import { Socket } from 'socket.io';
@WebSocketGateway()
export class ChatGateway {
@SubscribeMessage('join')
handleJoin(
@MessageBody('room') room: string,
@ConnectedSocket() client: Socket,
): void {
client.join(room);
client.emit('joined', { room, id: client.id });
}
}
Here client.emit sends an event only to the socket that triggered the handler, while client.join(room) subscribes it to a broadcast group.
Returning vs emitting responses
There are two ways to reply, and they behave differently. Returning a value (or a Promise/Observable) sends an acknowledgement back to the requesting client only — ideal for request/response semantics. Calling emit on a socket or server lets you target the sender, everyone, or a subset.
| Technique | Recipient | Use it for |
|---|---|---|
return value | The requesting client (as an ack) | One-to-one responses |
client.emit('evt', data) | The requesting client | Targeted push, custom event name |
client.broadcast.emit('evt', data) | Everyone except the sender | ”User joined” style notices |
server.emit('evt', data) | Every connected client | Global announcements |
server.to(room).emit('evt', data) | Clients in a room | Scoped fan-out |
To emit to all clients you need the server instance, exposed with @WebSocketServer().
import {
WebSocketGateway,
SubscribeMessage,
MessageBody,
ConnectedSocket,
WebSocketServer,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
@WebSocketGateway({ cors: { origin: '*' } })
export class ChatGateway {
@WebSocketServer()
server: Server;
@SubscribeMessage('chat')
handleChat(
@MessageBody() body: { room: string; text: string },
@ConnectedSocket() client: Socket,
): { delivered: boolean } {
// Fan out to everyone in the room...
this.server.to(body.room).emit('chat', {
from: client.id,
text: body.text,
});
// ...and acknowledge the sender via the return value.
return { delivered: true };
}
}
The return value becomes the Socket.IO acknowledgement callback argument on the client. If the client emits without an ack callback, the returned value is simply ignored — so use
emitwhen you need a guaranteed push.
Streaming multiple responses
Because handlers can return an Observable, a single inbound event can produce many outbound messages. Nest subscribes to the stream and emits each value back under the same event name.
import { SubscribeMessage, WebSocketGateway } from '@nestjs/websockets';
import { from, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@WebSocketGateway()
export class FeedGateway {
@SubscribeMessage('countdown')
countdown(): Observable<{ event: string; data: number }> {
return from([3, 2, 1]).pipe(
map((n) => ({ event: 'tick', data: n })),
);
}
}
Output:
client ← tick 3
client ← tick 2
client ← tick 1
Best Practices
- Treat gateways like controllers: keep them thin and delegate business logic to injected
@Injectableservices. - Return a value for request/response acknowledgements, and reserve
emitfor server-initiated pushes and broadcasts. - Type your payloads with interfaces or DTOs and validate them with pipes so handlers never trust raw socket input.
- Inject
@ConnectedSocket()only when you genuinely need the socket; otherwise keep handlers pure and easy to test. - Name events explicitly and consistently (
message,chat,join) and document the payload shape each one expects. - Scope broadcasts deliberately — prefer
server.to(room)overserver.emitto avoid leaking messages to unrelated clients.