WebSockets avec NestJS et Socket.io : créez des fonctionnalités temps réel
Notifications instantanées, chat en direct, mise à jour live d'un dashboard, indicateur "utilisateur en train d'écrire", flux de logs ou de commandes : dès qu'on veut afficher quelque chose sans recharger la page, le HTTP classique montre vite ses limites. Le polling consomme inutilement des ressources, et l'utilisateur sent toujours un léger décalage. La bonne réponse, c'est WebSockets — une connexion bidirectionnelle persistante entre client et serveur.
Bonne nouvelle : NestJS intègre nativement Socket.io via son module @nestjs/websockets, avec le même style décoratif que le reste du framework. Dans cet article, je vous montre comment construire une couche temps réel propre, typée et prête pour la production, avec des patterns que j'utilise sur mes projets clients.
Pourquoi WebSockets plutôt que polling ou SSE ?
Avant de coder, posons le décor. Trois approches existent pour faire du "temps réel" en web :
- Long polling : le client interroge le serveur en boucle. Simple, mais coûteux et latent.
- Server-Sent Events (SSE) : le serveur pousse vers le client en HTTP. Bien adapté aux flux unidirectionnels (logs, notifications passives).
- WebSockets : canal bidirectionnel persistant. Le bon choix dès qu'il y a interaction dans les deux sens (chat, collaboration, jeu, présence).
Socket.io ajoute par-dessus WebSockets un fallback automatique vers HTTP long polling, des rooms, des namespaces et un système d'acknowledgements. C'est mature, robuste, et NestJS s'y intègre parfaitement.
Installer et configurer le module WebSockets
On commence par installer les dépendances dans un projet NestJS existant :
npm install @nestjs/websockets @nestjs/platform-socket.io socket.io
Pas besoin d'enregistrer un module global : NestJS détecte automatiquement les gateways déclarés. Au démarrage, on peut tout de même configurer l'adaptateur Socket.io pour personnaliser CORS et options de transport :
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { IoAdapter } from '@nestjs/platform-socket.io';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useWebSocketAdapter(new IoAdapter(app));
app.enableCors({ origin: process.env.CLIENT_URL, credentials: true });
await app.listen(3000);
}
bootstrap();
Si votre frontend tourne sur un autre domaine (par exemple Next.js sur Vercel et NestJS sur Railway), n'oubliez pas le CORS Socket.io au niveau du gateway, qu'on verra plus bas.
Créer son premier Gateway
Un Gateway est l'équivalent d'un controller pour WebSockets. On le déclare avec @WebSocketGateway et on y attache des handlers via @SubscribeMessage.
// src/chat/chat.gateway.ts
import {
WebSocketGateway,
WebSocketServer,
SubscribeMessage,
MessageBody,
ConnectedSocket,
OnGatewayConnection,
OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { Logger } from '@nestjs/common';
@WebSocketGateway({
namespace: '/chat',
cors: { origin: process.env.CLIENT_URL, credentials: true },
})
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer()
server!: Server;
private readonly logger = new Logger(ChatGateway.name);
handleConnection(client: Socket): void {
this.logger.log(`Client connecté : ${client.id}`);
}
handleDisconnect(client: Socket): void {
this.logger.log(`Client déconnecté : ${client.id}`);
}
@SubscribeMessage('message:send')
handleMessage(
@MessageBody() payload: { roomId: string; text: string },
@ConnectedSocket() client: Socket,
): void {
this.server.to(payload.roomId).emit('message:new', {
id: crypto.randomUUID(),
text: payload.text,
from: client.data.userId,
sentAt: new Date().toISOString(),
});
}
}
Quelques points à retenir : le décorateur @WebSocketServer() injecte l'instance Server Socket.io, indispensable pour broadcaster. Les events sont typés par convention (message:send, message:new) — adoptez un namespace cohérent, ça évite vite le chaos.
N'oubliez pas de déclarer le gateway dans son module :
// src/chat/chat.module.ts
import { Module } from '@nestjs/common';
import { ChatGateway } from './chat.gateway';
@Module({ providers: [ChatGateway] })
export class ChatModule {}
Authentifier les connexions WebSocket
C'est l'erreur la plus fréquente que je vois : un gateway public, sans authentification. Sur HTTP, vos guards JWT protègent les routes ; sur WebSockets, il faut faire le travail explicitement.
Le pattern propre consiste à valider le token au moment du handshake via un middleware Socket.io :
// src/chat/ws-auth.middleware.ts
import { Socket } from 'socket.io';
import { JwtService } from '@nestjs/jwt';
export const wsAuthMiddleware =
(jwt: JwtService) =>
(socket: Socket, next: (err?: Error) => void) => {
try {
const token =
socket.handshake.auth?.token ??
socket.handshake.headers.authorization?.replace('Bearer ', '');
if (!token) throw new Error('Token manquant');
const payload = jwt.verify<{ sub: string }>(token);
socket.data.userId = payload.sub;
next();
} catch {
next(new Error('Non autorisé'));
}
};
Et on l'enregistre dans le gateway via afterInit :
import { OnGatewayInit } from '@nestjs/websockets';
@WebSocketGateway({ namespace: '/chat' })
export class ChatGateway implements OnGatewayInit {
constructor(private readonly jwt: JwtService) {}
afterInit(server: Server): void {
server.use(wsAuthMiddleware(this.jwt));
}
}
Résultat : aucun client ne peut s'abonner sans un JWT valide, et socket.data.userId est disponible dans tous vos handlers. Si vous gérez des rôles, jetez un œil à mon article sur les Guards NestJS avec JWT et rôles — la même logique s'applique côté WebSockets.
Rooms, namespaces et broadcast ciblé
Le vrai pouvoir de Socket.io, ce sont les rooms. Une room, c'est un groupe nommé auquel on peut envoyer des messages sans toucher les autres clients. Indispensable pour un chat multi-conversation, un éditeur collaboratif ou un dashboard multi-utilisateurs.
@SubscribeMessage('room:join')
async handleJoinRoom(
@MessageBody() roomId: string,
@ConnectedSocket() client: Socket,
): Promise<{ ok: true }> {
await client.join(roomId);
this.server.to(roomId).emit('room:user-joined', {
userId: client.data.userId,
});
return { ok: true };
}
Le return ici sert d'acknowledgement : Socket.io permet au client d'attendre une réponse synchrone, ce qui simplifie la gestion d'erreurs côté frontend. Côté client :
const ack = await socket.emitWithAck('room:join', 'room-42');
Les namespaces (/chat, /admin, /notifications), eux, séparent logiquement vos cas d'usage et permettent d'avoir des middlewares d'authentification différents par contexte.
Valider les payloads avec un Pipe
Un message qui arrive du client n'est pas plus sûr qu'un body HTTP. Appliquez les mêmes règles : validez avec class-validator et un ValidationPipe.
import { IsString, MaxLength, IsUUID } from 'class-validator';
import { UsePipes, ValidationPipe } from '@nestjs/common';
class SendMessageDto {
@IsUUID()
roomId!: string;
@IsString()
@MaxLength(2000)
text!: string;
}
@SubscribeMessage('message:send')
@UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }))
handleMessage(@MessageBody() dto: SendMessageDto, @ConnectedSocket() client: Socket) {
// dto est déjà validé et typé
}
Petit piège : si la validation échoue, NestJS émet une WsException. Le client doit donc écouter l'event exception ou utiliser emitWithAck pour récupérer l'erreur proprement.
Côté Next.js : se connecter au gateway
Côté frontend, l'intégration tient en quelques lignes. Pensez à isoler le client dans un hook pour éviter de réouvrir une connexion à chaque render :
'use client';
import { useEffect, useRef } from 'react';
import { io, Socket } from 'socket.io-client';
export function useChatSocket(token: string) {
const socketRef = useRef<Socket | null>(null);
useEffect(() => {
const socket = io(`${process.env.NEXT_PUBLIC_API_URL}/chat`, {
auth: { token },
transports: ['websocket'],
withCredentials: true,
});
socketRef.current = socket;
return () => {
socket.disconnect();
};
}, [token]);
return socketRef;
}
Important : la connexion WebSocket vit côté client. C'est un cas typique où vos composants Next.js doivent être marqués 'use client'. Pour un rappel sur la frontière Server/Client, voir mon article React Server Components vs Client Components.
Scaler avec plusieurs instances : l'adaptateur Redis
Un gateway sur une seule instance, c'est facile. Dès que vous passez en production avec plusieurs replicas, vous tombez sur ce problème : un message émis par l'instance A n'atteint pas les clients connectés à l'instance B.
La solution standard est l'adaptateur Redis :
npm install @socket.io/redis-adapter ioredis
// src/redis-io.adapter.ts
import { IoAdapter } from '@nestjs/platform-socket.io';
import { ServerOptions } from 'socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';
import { INestApplicationContext } from '@nestjs/common';
export class RedisIoAdapter extends IoAdapter {
private adapterConstructor!: ReturnType<typeof createAdapter>;
constructor(app: INestApplicationContext) {
super(app);
}
async connectToRedis(): Promise<void> {
const pub = createClient({ url: process.env.REDIS_URL });
const sub = pub.duplicate();
await Promise.all([pub.connect(), sub.connect()]);
this.adapterConstructor = createAdapter(pub, sub);
}
createIOServer(port: number, options?: ServerOptions) {
const server = super.createIOServer(port, options);
server.adapter(this.adapterConstructor);
return server;
}
}
À enregistrer dans main.ts à la place de l'IoAdapter standard. Sans cette étape, votre infrastructure ne tient pas la charge dès qu'un load balancer entre en jeu.
Bonnes pratiques que j'applique systématiquement
Pour finir, voici la checklist que j'utilise sur mes projets temps réel :
- Authentifier dès le handshake, jamais dans chaque event séparément.
- Nommer les events avec un namespace (
message:send,room:join) pour rester lisible. - Valider tous les payloads avec un DTO +
ValidationPipe, comme sur HTTP. - Utiliser les acknowledgements pour les actions critiques (création, validation).
- Limiter le débit côté serveur avec un rate limiter par socket pour éviter les abus.
- Logger les connexions/déconnexions : ça sauve la vie en prod quand on cherche pourquoi un client se reconnecte en boucle.
- Prévoir Redis dès le départ si vous savez que vous scalerez horizontalement.
- Garder les gateways minces : déléguez la logique métier à des services NestJS classiques, comme pour les controllers.
Conclusion
WebSockets transforment l'expérience utilisateur dès qu'on touche à la collaboration, à la notification ou au temps réel. NestJS rend leur intégration aussi propre que le reste du framework — gateways, DI, pipes, guards : vous gardez la même cohérence architecturale du code HTTP au code WebSocket.
Le vrai défi n'est pas de "faire fonctionner" Socket.io, mais de le faire bien : sécurité au handshake, validation systématique, scalabilité avec Redis, et nommage rigoureux des events. Avec ces fondations, vous pouvez bâtir un chat, un dashboard live ou un système de présence sans dette technique.
Vous avez un projet qui demande du temps réel — chat client, notifications, dashboard collaboratif, suivi de commande live ? N'hésitez pas à me contacter pour en discuter, je serai ravi de vous accompagner.