Real-time communication is non-negotiable in modern web platforms—whether you're tracking order status, pushing notifications, or syncing collaborative edits. WebSockets with NestJS gateways let you build these features cleanly by leveraging the framework's dependency injection and decorator-driven architecture, the same patterns you use for HTTP controllers and services.
I've built real-time features across e-commerce, ride-hailing, and SaaS platforms. The moment you need to push data from the server to clients without polling, you reach for WebSockets. NestJS makes this natural through its gateway abstraction, which wraps Socket.IO and integrates fully with your application container. In this post, I'll walk you through implementing WebSockets with NestJS gateways—from wiring up a basic gateway to authenticating handshakes and pushing events from your domain services.
What is a gateway, and why it's just another provider
A gateway in NestJS is a provider decorated with @WebSocketGateway(). Like controllers and services, it's instantiated by the DI container and can inject dependencies. The key difference is that instead of handling HTTP requests, it manages WebSocket connections and events through Socket.IO.
Think of a gateway as a bridge between your connected clients and your business logic. When a client connects via WebSocket, the gateway receives connection and message events. You then decide what to do—join a room, emit to other clients, or call a service to update state.
Here's a minimal example for tracking orders:
import { WebSocketGateway, WebSocketServer, SubscribeMessage, MessageBody, ConnectedSocket } from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
@WebSocketGateway({ namespace: 'orders' })
export class OrdersGateway {
@WebSocketServer() server: Server;
@SubscribeMessage('track')
track(@MessageBody() id: string, @ConnectedSocket() socket: Socket) {
socket.join('order:' + id);
socket.emit('tracking', { orderId: id, status: 'joined' });
}
}The @WebSocketGateway({ namespace: 'orders' }) decorator registers this class as a WebSocket provider on the /orders namespace. The @WebSocketServer() decorator injects the Socket.IO server instance. The @SubscribeMessage('track') method listens for the track event emitted by clients.
The important part: OrdersGateway is a provider. You can inject services into it just like you would a controller:
@WebSocketGateway({ namespace: 'orders' })
export class OrdersGateway {
@WebSocketServer() server: Server;
constructor(private ordersService: OrdersService) {}
@SubscribeMessage('track')
async track(@MessageBody() id: string, @ConnectedSocket() socket: Socket) {
const order = await this.ordersService.findOne(id);
if (order) {
socket.join('order:' + id);
socket.emit('tracking', { orderId: id, status: order.status });
}
}
}This gateway now has access to your business logic. When a client requests to track an order, you verify it exists before letting them join the room. Clean, testable, and aligned with NestJS patterns.
Rooms for targeting specific clients
Socket.IO rooms are a lightweight way to group clients and broadcast to a subset without iterating through every connection. In my ride-hailing platform, we used rooms to push ride updates to only the driver and passenger—not every connected user.
When a client calls socket.join(roomName), they're added to that room. You can then emit to the room with server.to(roomName).emit(event, data).
Let's expand the orders example. When an order is delivered, we want to notify only the customer who is tracking it:
@WebSocketGateway({ namespace: 'orders' })
export class OrdersGateway {
@WebSocketServer() server: Server;
constructor(private ordersService: OrdersService) {}
@SubscribeMessage('track')
async track(@MessageBody() id: string, @ConnectedSocket() socket: Socket) {
const order = await this.ordersService.findOne(id);
if (order) {
socket.join('order:' + id);
socket.emit('tracking', { orderId: id, status: order.status });
}
}
notifyOrderUpdate(orderId: string, status: string) {
this.server.to('order:' + orderId).emit('statusUpdate', { orderId, status });
}
}The notifyOrderUpdate method broadcasts to the order:{id} room. Only clients who called track and joined that room receive the event. This is far more efficient than emitting to every connected socket.
You can also emit to all clients except the sender with socket.broadcast.emit(), or to all clients with this.server.emit(). Rooms let you be surgical about who gets what message.
Authenticating the WebSocket handshake
Real-time features often need security. You don't want arbitrary clients joining order-tracking rooms. JWT authentication on the WebSocket handshake is essential.
NestJS gateways support a handshake guard or middleware. The simplest approach is an OnGatewayConnection hook that validates the token before allowing the client to stay connected:
import { WebSocketGateway, OnGatewayConnection, WebSocketServer, SubscribeMessage, MessageBody, ConnectedSocket } from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { JwtService } from '@nestjs/jwt';
@WebSocketGateway({ namespace: 'orders' })
export class OrdersGateway implements OnGatewayConnection {
@WebSocketServer() server: Server;
constructor(
private ordersService: OrdersService,
private jwtService: JwtService,
) {}
async handleConnection(socket: Socket) {
try {
const token = socket.handshake.auth.token || socket.handshake.headers.authorization?.split(' ')[1];
if (!token) {
socket.disconnect();
return;
}
const payload = this.jwtService.verify(token);
socket.data.userId = payload.sub;
} catch (err) {
socket.disconnect();
}
}
@SubscribeMessage('track')
async track(@MessageBody() id: string, @ConnectedSocket() socket: Socket) {
const order = await this.ordersService.findOne(id);
// Check that the user owns this order
if (order.userId !== socket.data.userId) {
socket.emit('error', { message: 'Unauthorized' });
return;
}
socket.join('order:' + id);
socket.emit('tracking', { orderId: id, status: order.status });
}
}The handleConnection hook is called when a client connects. We extract the token from the handshake (either in auth or headers), verify it with the JWT service, and store the user ID on the socket object. If verification fails, we disconnect immediately.
In the track method, we verify that the requesting user actually owns the order before allowing them to join the room. This prevents data leakage between users.
For more sophisticated scenarios, you can also create a custom gateway guard using the same @UseGuards() decorator that works with HTTP endpoints, though the connection-level validation I've shown is more common in practice.
Pushing events from services
Here's where the real power of NestJS gateways shines: you can inject the gateway into your domain services and emit events when business logic changes.
Imagine an order processing service. When an order status changes—say, from "pending" to "shipped"—we want to notify the customer immediately:
@Injectable()
export class OrdersService {
constructor(
private db: Database,
private ordersGateway: OrdersGateway,
) {}
async updateStatus(orderId: string, status: string) {
await this.db.orders.update(orderId, { status });
// Push the update to all connected clients tracking this order
this.ordersGateway.notifyOrderUpdate(orderId, status);
}
}The service doesn't know or care that it's talking to WebSockets—it just calls a method on the gateway. The gateway handles the Socket.IO specifics. This decoupling is crucial for maintainability.
In the e-commerce platform I worked on, this pattern let us push order updates, payment confirmations, and shipment tracking without any special middleware or event bus. For more complex event workflows across services, you might use a message queue like Kafka, but for direct server-to-client updates in a single app, injecting the gateway is straightforward and performant.
Make sure to register the gateway in your module:
@Module({
providers: [OrdersGateway, OrdersService],
})
export class OrdersModule {}Common mistakes
Forgetting to join a room before emitting to it. If you emit to a room that no client has joined, nothing happens silently. Always verify your room join logic in handlers.
Leaking user data by emitting to all clients. I've seen developers use this.server.emit() when they meant this.server.to(roomName).emit(). Always use rooms for user-specific data.
Not handling disconnections. Implement OnGatewayDisconnect if you need to clean up state—for example, removing a user from a tracking list or ending a session.
Blocking the event loop in message handlers. If your @SubscribeMessage() handler does heavy computation or blocking I/O, it'll freeze updates for other clients. Use async/await and keep handlers lightweight.
Testing gateways without Socket.IO client libraries. Unit testing gateways is tricky. Use socket.io-client in integration tests or mock the gateway in service tests.
Key takeaways
- Gateways are NestJS providers. They integrate with your DI container, letting you inject services and keep your real-time logic colocated with domain code.
- The
@WebSocketGateway()decorator registers a provider on a Socket.IO namespace;@WebSocketServer()injects the server instance. - Rooms are your primary tool for targeting subsets of clients. Use
socket.join()to add clients andserver.to(roomName).emit()to broadcast. - Validate JWT tokens in the
OnGatewayConnectionhook or a custom guard. Store the authenticated user ID on the socket object for later authorization checks. - Inject gateways into services to push updates when business logic changes. This keeps real-time features decoupled from domain code and highly testable.
- Always scope broadcasts to specific rooms or users. Emitting to all clients is rarely correct and leaks data.






Comments
Loading comments…