Hầu hết các khái niệm được đề cập ở những phần khác trong tài liệu này, như tiêm phụ thuộc (dependency injection), decorator, exception filter, pipe, guard và interceptor, đều áp dụng tương tự cho gateway. Ở bất cứ đâu có thể, Nest trừu tượng hóa các chi tiết triển khai để cùng một component có thể chạy trên các platform dựa trên HTTP, WebSockets và Microservices. Phần này trình bày các khía cạnh của Nest đặc thù cho WebSockets.
Trong Nest, một gateway đơn giản là một class được đánh dấu bằng decorator @WebSocketGateway(). Về mặt kỹ thuật, gateway không phụ thuộc vào platform cụ thể (platform-agnostic), điều này giúp chúng tương thích với bất kỳ thư viện WebSockets nào miễn là đã có adapter tương ứng. Có hai platform WS được hỗ trợ sẵn: socket.io và ws. Bạn có thể chọn cái phù hợp nhất với nhu cầu của mình. Ngoài ra, bạn cũng có thể tự xây adapter riêng theo hướng dẫn này.

Gợi ý Gateway có thể được xem như provider; nghĩa là chúng có thể inject dependency qua constructor của class. Ngoài ra, gateway cũng có thể được inject vào các class khác (provider và controller).
Cài đặt
Để bắt đầu xây dựng ứng dụng dựa trên WebSockets, trước tiên hãy cài package cần thiết:
$ npm i --save @nestjs/websockets @nestjs/platform-socket.io
$ npm i --save @nestjs/websockets @nestjs/platform-socket.io
Tổng quan
Thông thường, mỗi gateway lắng nghe trên cùng port với HTTP server, trừ khi app của bạn không phải là web application, hoặc bạn đã đổi port bằng tay. Hành vi mặc định này có thể thay đổi bằng cách truyền một argument vào decorator @WebSocketGateway(80), trong đó 80 là số port bạn chọn. Bạn cũng có thể đặt namespace mà gateway sử dụng theo cú pháp sau:
@WebSocketGateway(80, { namespace: 'events' })
Cảnh báo Gateway sẽ không được khởi tạo (instantiate) cho tới khi chúng được tham chiếu trong mảng providers của một module đang tồn tại.
Bạn có thể truyền bất kỳ option nào được hỗ trợ cho socket constructor thông qua argument thứ hai của decorator @WebSocketGateway(), như dưới đây:
@WebSocketGateway(81, { transports: ['websocket'] })
Gateway giờ đã lắng nghe, nhưng chúng ta vẫn chưa subscribe vào message nào cả. Hãy tạo một handler subscribe vào message events và phản hồi lại cho user chính xác dữ liệu đó.
@SubscribeMessage('events')
handleEvent(@MessageBody() data: string): string {
return data;
}
@Bind(MessageBody())
@SubscribeMessage('events')
handleEvent(data) {
return data;
}
Gợi ý Decorator @SubscribeMessage() và @MessageBody() được import từ package @nestjs/websockets.
Sau khi gateway được tạo, chúng ta có thể đăng ký nó vào module của mình.
import { Module } from '@nestjs/common';
import { EventsGateway } from './events.gateway.js';
@Module({
providers: [EventsGateway]
})
export class EventsModule {}
Bạn cũng có thể truyền một property key vào decorator để trích xuất nó từ message body đến:
@SubscribeMessage('events')
handleEvent(@MessageBody('id') id: number): number {
// id === messageBody.id
return id;
}
@Bind(MessageBody('id'))
@SubscribeMessage('events')
handleEvent(id) {
// id === messageBody.id
return id;
}
Nếu bạn không muốn dùng decorator, đoạn code sau tương đương về mặt chức năng:
@SubscribeMessage('events')
handleEvent(client: Socket, data: string): string {
return data;
}
@SubscribeMessage('events')
handleEvent(client, data) {
return data;
}
Trong ví dụ trên, hàm handleEvent() nhận hai argument. Argument đầu tiên là một socket instance đặc thù cho platform, còn argument thứ hai là dữ liệu nhận từ client. Tuy nhiên cách này không được khuyến nghị, vì nó yêu cầu bạn phải mock instance socket trong mỗi unit test.
Khi message events được nhận, handler gửi một acknowledgment (xác nhận) kèm chính dữ liệu đã được gửi qua network. Ngoài ra, bạn cũng có thể emit message theo cách đặc thù của thư viện, ví dụ dùng method client.emit(). Để truy cập socket instance đang kết nối, dùng decorator @ConnectedSocket().
@SubscribeMessage('events')
handleEvent(
@MessageBody() data: string,
@ConnectedSocket() client: Socket,
): string {
return data;
}
@Bind(MessageBody(), ConnectedSocket())
@SubscribeMessage('events')
handleEvent(data, client) {
return data;
}
Gợi ý Decorator @ConnectedSocket() được import từ package @nestjs/websockets.
Tuy nhiên, trong trường hợp này, bạn sẽ không thể tận dụng interceptor. Nếu bạn không muốn phản hồi lại cho user, bạn có thể đơn giản là bỏ statement return (hoặc trả về rõ ràng một giá trị “falsy”, ví dụ undefined).
Bây giờ khi client emit message như sau:
socket.emit('events', { name: 'Nest' });
Method handleEvent() sẽ được thực thi. Để lắng nghe các message được emit từ trong handler ở trên, client phải gắn thêm một acknowledgment listener tương ứng:
socket.emit('events', { name: 'Nest' }, (data) => console.log(data));
Trong khi việc return một giá trị từ message handler sẽ ngầm gửi một acknowledgement, các kịch bản nâng cao thường yêu cầu kiểm soát trực tiếp acknowledgement callback.
Decorator parameter @Ack() cho phép bạn inject trực tiếp hàm callback ack vào message handler.
Nếu không dùng decorator, callback này được truyền vào như argument thứ ba của method.
@SubscribeMessage('events')
handleEvent(
@MessageBody() data: string,
@Ack() ack: (response: { status: string; data: string }) => void,
) {
ack({ status: 'received', data });
}
@Bind(MessageBody(), Ack())
@SubscribeMessage('events')
handleEvent(data, ack) {
ack({ status: 'received', data });
}
Ghi chú người dịch Nếu bạn từng làm việc với Express, ack ở đây giống ý tưởng của một callback bạn tự viết trong handler socket.io thuần (socket.on('event', (data, callback) => ...)), chỉ khác là Nest bọc nó lại thành decorator để tách biệt khỏi tham số vị trí. Điểm cần nhớ: nếu bạn gọi ack() thủ công thì đừng return giá trị nữa, tránh gửi acknowledgment hai lần.
Nhiều phản hồi (Multiple responses)
Acknowledgment chỉ được gửi đi đúng một lần. Hơn nữa, native WebSockets implementation không hỗ trợ điều này. Để giải quyết hạn chế này, bạn có thể return một object gồm hai thuộc tính: event là tên của event được emit, và data là dữ liệu cần chuyển tiếp tới client.
@SubscribeMessage('events')
handleEvent(@MessageBody() data: unknown): WsResponse<unknown> {
const event = 'events';
return { event, data };
}
@Bind(MessageBody())
@SubscribeMessage('events')
handleEvent(data) {
const event = 'events';
return { event, data };
}
Gợi ý Interface WsResponse được import từ package @nestjs/websockets.
Cảnh báo Bạn nên return một class instance implement WsResponse nếu field data của bạn phụ thuộc vào ClassSerializerInterceptor, vì interceptor này bỏ qua các response là plain JavaScript object.
Để lắng nghe response đến, client phải gắn thêm một event listener khác.
socket.on('events', (data) => console.log(data));
Phản hồi bất đồng bộ (Asynchronous responses)
Message handler có thể phản hồi đồng bộ hoặc bất đồng bộ. Do đó, các method async đều được hỗ trợ. Message handler cũng có thể return một Observable, trong trường hợp này các giá trị kết quả sẽ được emit liên tục cho tới khi stream hoàn tất.
@SubscribeMessage('events')
onEvent(@MessageBody() data: unknown): Observable<WsResponse<number>> {
const event = 'events';
const response = [1, 2, 3];
return from(response).pipe(
map(data => ({ event, data })),
);
}
@Bind(MessageBody())
@SubscribeMessage('events')
onEvent(data) {
const event = 'events';
const response = [1, 2, 3];
return from(response).pipe(
map(data => ({ event, data })),
);
}
Trong ví dụ trên, message handler sẽ phản hồi 3 lần (với mỗi item trong array).
Sự kiện vòng đời (Lifecycle hook)
Có 3 lifecycle hook hữu ích khả dụng. Tất cả đều có interface tương ứng và được mô tả trong bảng sau:
OnGatewayInit
|
Buộc bạn phải implement method afterInit(). Nhận vào server instance đặc thù cho thư viện đang dùng (và
spread các argument còn lại nếu cần).
|
OnGatewayConnection
|
Buộc bạn phải implement method handleConnection(). Nhận vào client socket instance đặc thù cho thư viện đang dùng như
một argument.
|
OnGatewayDisconnect
|
Buộc bạn phải implement method handleDisconnect(). Nhận vào client socket instance đặc thù cho thư viện đang dùng như
một argument.
|
Gợi ý Mỗi lifecycle interface đều được export từ package @nestjs/websockets.
Ghi chú người dịch Ba hook này tương ứng với ba mốc bạn hay tự viết bằng tay khi dùng socket.io thuần: io.on('connection', ...) (→ handleConnection), socket.on('disconnect', ...) (→ handleDisconnect), và đoạn code chạy ngay sau khi tạo server (→ afterInit). Nest chỉ chuẩn hóa chúng thành lifecycle hook để bạn không phải nhớ wire tay từng chỗ.
Server và Namespace
Đôi khi, bạn có thể muốn truy cập trực tiếp tới server instance gốc, đặc thù cho platform. Tham chiếu tới object này được truyền vào như một argument của method afterInit() (interface OnGatewayInit). Một cách khác là dùng decorator @WebSocketServer().
@WebSocketServer()
server: Server;
Ngoài ra, bạn có thể lấy namespace tương ứng bằng thuộc tính namespace, như sau:
@WebSocketGateway({ namespace: 'my-namespace' })
export class EventsGateway {
@WebSocketServer()
namespace: Namespace;
}
Decorator @WebSocketServer() inject một server instance bằng cách tham chiếu tới metadata được lưu bởi decorator @WebSocketGateway(). Nếu bạn cung cấp option namespace cho decorator @WebSocketGateway(), decorator @WebSocketServer() sẽ trả về một instance Namespace thay vì instance Server.
Lưu ý Decorator @WebSocketServer() được import từ package @nestjs/websockets.
Nest sẽ tự động gán server instance cho property này ngay khi nó sẵn sàng để dùng.
Gateway theo phạm vi request (Request-scoped gateway)
Kể từ NestJS v12, gateway hỗ trợ provider theo phạm vi request (request-scoped). Một instance mới của mỗi dependency request-scoped được tạo cho từng socket đang kết nối, và instance đó sống trong suốt thời gian connection còn tồn tại — nên nó có thể lưu state riêng cho từng connection một cách an toàn.
Inject chính socket đó bằng token REQUEST, giống hệt cách bạn inject HTTP request trong một provider request-scoped ở HTTP:
import { Inject, Injectable, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Socket } from 'socket.io';
@Injectable({ scope: Scope.REQUEST })
export class ConnectionStateService {
private sequence = 0;
constructor(@Inject(REQUEST) private readonly client: Socket) {}
next() {
return { clientId: this.client.id, sequence: ++this.sequence };
}
}
Sau đó gateway inject nó như một provider bình thường:
@WebSocketGateway()
export class EventsGateway {
constructor(private readonly connectionState: ConnectionStateService) {}
handleConnection(@ConnectedSocket() client: Socket) {
client.emit('connected', this.connectionState.next());
}
@SubscribeMessage('state')
onState() {
return { event: 'state', data: this.connectionState.next() };
}
}
Vì scope ở đây gắn với connection thay vì gắn với một message đơn lẻ, bộ đếm sequence ở trên sẽ tăng lên qua mọi message nhận được trên socket đó, trong khi một client thứ hai sẽ có instance độc lập riêng của nó. Nest sẽ tear down (dọn dẹp) các instance request-scoped này khi socket disconnect.
Lưu ý Giống như với HTTP, provider request-scoped làm tăng chi phí khởi tạo (instantiation overhead) cho mỗi connection. Chỉ dùng khi bạn thực sự cần state riêng theo từng connection, còn lại nên giữ scope singleton mặc định.
Ghi chú người dịch Nếu bạn từng dùng Go, cơ chế này giống việc bạn tạo một context.Context (hoặc một struct state) mới cho mỗi connection và truyền nó xuống các hàm xử lý — chỉ khác là Nest tự động hóa việc tạo/hủy instance đó qua DI container thay vì bạn tự quản lý bằng tay. Với PHP/Laravel, nó tương tự binding một service vào “request scope” trong service container, chỉ khác đơn vị scope ở đây là một kết nối WebSocket, không phải một HTTP request.
Ví dụ
Một ví dụ hoạt động đầy đủ có sẵn tại đây.