Documentación de WebSockets
A diferencia de una API REST, donde el contrato se define como pares petición/respuesta sobre HTTP, los WebSockets establecen un canal bidireccional persistente en el que tanto el cliente como el servidor pueden emitir mensajes en cualquier momento. Esto introduce retos específicos de documentación:
- No existe un "método + ruta" como referencia visual.
- Los mensajes no siguen un orden fijo (son eventos asíncronos).
- Tanto el cliente como el servidor son emisores y receptores.
- Es necesario documentar el ciclo de vida de la conexión (conexión, reconexión, desconexión).
El estándar para documentar este tipo de sistemas es AsyncAPI.
AsyncAPI
AsyncAPI es la especificación equivalente a OpenAPI para sistemas orientados a eventos y mensajería asíncrona. Es compatible con WebSockets, MQTT (Message Queuing Telemetry Transport), AMQP (Advanced Message Queuing Protocol), Kafka y otros protocolos.
Un documento AsyncAPI describe:
- Los canales (análogos a los topics o nombres de eventos).
- Los mensajes que se publican o suscriben en cada canal.
- Los schemas de los datos de cada mensaje.
- Los bindings específicos del protocolo (por ejemplo, WebSocket).
Estructura básica de AsyncAPI para WebSockets:
asyncapi: 2.6.0
info:
title: API WebSocket - Chat en tiempo real
version: 1.0.0
description: |
Documentación de los eventos WebSocket del sistema de chat.
Utiliza Socket.IO sobre WebSocket nativo.
servers:
development:
url: ws://localhost:3000
protocol: ws
description: Servidor de desarrollo
production:
url: wss://api.ejemplo.com
protocol: wss
description: Servidor de producción
channels:
chat/message:
description: Canal para el envío y recepción de mensajes de chat
publish:
summary: El cliente envía un mensaje al servidor
message:
$ref: '#/components/messages/ChatMessage'
subscribe:
summary: El servidor distribuye un mensaje a los clientes de la sala
message:
$ref: '#/components/messages/ChatMessage'
components:
messages:
ChatMessage:
payload:
type: object
required:
- roomId
- content
properties:
roomId:
type: string
description: ID de la sala de chat
example: "room-42"
content:
type: string
description: Contenido del mensaje
example: "¡Hola a todos!"
senderId:
type: string
description: ID del usuario que envía el mensaje
timestamp:
type: string
format: date-time
Documentación con Socket.IO
Socket.IO es la librería más usada para WebSockets en Node.js. Añade abstracción sobre el protocolo nativo con salas, namespaces, reconexión automática y eventos personalizados.
Convención de documentación en el código
La mejor práctica es usar comentarios estructurados junto a cada manejador de eventos:
/**
* @module ChatSocket
* @description Manejadores de eventos WebSocket para el sistema de chat.
*/
/**
* Registra los manejadores de eventos de chat para un socket dado.
*
* @param {import('socket.io').Socket} socket - Socket del cliente conectado.
* @param {import('socket.io').Server} io - Instancia del servidor Socket.IO.
*/
function registerChatHandlers(socket, io) {
/**
* @event client:join_room
* @description El cliente solicita unirse a una sala de chat.
*
* @param {Object} payload
* @param {string} payload.roomId - ID de la sala a la que unirse.
* @param {string} payload.username - Nombre visible del usuario.
*
* @emits server:user_joined - Notifica a la sala que un usuario se ha unido.
*/
socket.on('join_room', ({ roomId, username }) => {
socket.join(roomId);
socket.to(roomId).emit('user_joined', {
userId: socket.id,
username,
timestamp: new Date().toISOString(),
});
});
/**
* @event client:send_message
* @description El cliente envía un mensaje a una sala.
*
* @param {Object} payload
* @param {string} payload.roomId - ID de la sala destinataria.
* @param {string} payload.content - Contenido del mensaje (máx. 500 caracteres).
*
* @emits server:new_message - Distribuye el mensaje a todos los miembros de la sala.
*/
socket.on('send_message', ({ roomId, content }) => {
const message = {
id: crypto.randomUUID(),
senderId: socket.id,
roomId,
content,
timestamp: new Date().toISOString(),
};
io.to(roomId).emit('new_message', message);
});
/**
* @event client:leave_room
* @description El cliente abandona una sala de chat.
*
* @param {Object} payload
* @param {string} payload.roomId - ID de la sala que se abandona.
*
* @emits server:user_left - Notifica a la sala que el usuario ha salido.
*/
socket.on('leave_room', ({ roomId }) => {
socket.leave(roomId);
socket.to(roomId).emit('user_left', {
userId: socket.id,
timestamp: new Date().toISOString(),
});
});
}
module.exports = { registerChatHandlers };
Catálogo de eventos: tabla de referencia
Una forma muy práctica de documentar WebSockets es mantener una tabla de eventos en el README.md o en un fichero Markdown dedicado. Es más rápida de consultar que un fichero YAML completo.
Eventos emitidos por el cliente
| Evento | Descripción | Payload |
|---|---|---|
join_room | Solicita unirse a una sala | { roomId: string, username: string } |
leave_room | Abandona una sala | { roomId: string } |
send_message | Envía un mensaje a una sala | { roomId: string, content: string } |
typing | Indica que el usuario está escribiendo | { roomId: string } |
stop_typing | Indica que el usuario dejó de escribir | { roomId: string } |
Eventos emitidos por el servidor
| Evento | Destinatario | Descripción | Payload |
|---|---|---|---|
user_joined | Sala (excepto emisor) | Un usuario se unió | { userId, username, timestamp } |
user_left | Sala (excepto emisor) | Un usuario abandonó | { userId, timestamp } |
new_message | Sala completa | Nuevo mensaje en la sala | { id, senderId, roomId, content, timestamp } |
error | Solo el cliente | Error del servidor | { code: string, message: string } |
Ciclo de vida de la conexión
Es fundamental documentar el ciclo de vida completo del socket, especialmente los eventos nativos de conexión y desconexión:
/**
* Punto de entrada principal para todos los sockets conectados.
*
* Ciclo de vida:
* 1. El cliente se conecta → se dispara 'connection'.
* 2. Se registran los manejadores de eventos del cliente.
* 3. Si se pierde la conexión, Socket.IO intenta reconectar automáticamente.
* 4. Al desconectarse definitivamente → se dispara 'disconnect'.
*
* @param {import('socket.io').Server} io - Instancia del servidor Socket.IO.
*/
function setupSockets(io) {
io.on('connection', (socket) => {
console.log(`Cliente conectado: ${socket.id}`);
// Autenticación al conectar (mediante handshake)
const token = socket.handshake.auth.token;
if (!isValidToken(token)) {
socket.disconnect(true);
return;
}
// Registrar manejadores
registerChatHandlers(socket, io);
/**
* @event disconnect
* @description Se dispara cuando el cliente pierde la conexión.
* @param {string} reason - Motivo de la desconexión.
* Posibles valores: 'transport close', 'server namespace disconnect',
* 'ping timeout', 'transport error'.
*/
socket.on('disconnect', (reason) => {
console.log(`Cliente desconectado: ${socket.id} — Motivo: ${reason}`);
});
});
}
Namespaces y salas
Socket.IO permite organizar los sockets en namespaces (canales lógicos independientes) y salas (grupos dentro de un namespace). Documentar esta estructura es clave:
Servidor Socket.IO
│
├── Namespace: / (por defecto)
│ ├── Sala: room-1
│ ├── Sala: room-2
│ └── Sala: room-N
│
├── Namespace: /notifications
│ └── (cada usuario está en su propia sala: userId)
│
└── Namespace: /admin
└── (acceso restringido por middleware de autenticación)
/**
* @namespace /notifications
* @description Namespace dedicado a notificaciones en tiempo real.
* Cada usuario autenticado se une automáticamente a una sala
* con su propio userId para recibir notificaciones personales.
*
* Eventos del servidor → cliente:
* - `notification:new` — Nueva notificación pendiente.
* - `notification:clear` — Todas las notificaciones marcadas como leídas.
*/
const notifNamespace = io.of('/notifications');
Manejo de errores
Los errores en WebSockets deben documentarse explícitamente, ya que no existe el concepto de "código de estado HTTP":
/**
* Emite un evento de error estandarizado al cliente.
*
* @param {import('socket.io').Socket} socket - Socket del cliente afectado.
* @param {string} code - Código de error legible por máquina (p.ej. 'ROOM_NOT_FOUND').
* @param {string} message - Descripción legible por humanos.
*
* @example
* // Estructura del evento 'error' recibido por el cliente:
* // { code: 'UNAUTHORIZED', message: 'Token de autenticación inválido' }
*/
function emitError(socket, code, message) {
socket.emit('error', { code, message });
}
Catálogo de errores
UNAUTHORIZED- El token JWT es inválido o ha expirado.ROOM_NOT_FOUND- La sala indicada no existe.ROOM_FULL- La sala ha alcanzado el límite de participantes.MESSAGE_TOO_LONG- El contenido del mensaje supera 500 caracteres.RATE_LIMITED- El cliente ha superado el límite de mensajes por minuto.
Buenas prácticas
- Documenta tanto la dirección como el destinatario. Para cada evento, especifica si va de cliente a servidor, de servidor a cliente, o es bidireccional, y si el destinatario es un cliente individual, una sala o todos los clientes.
- Define los schemas de los payloads. Un payload ambiguo es la principal fuente de bugs en sistemas WebSocket.
- Documenta el comportamiento ante reconexión. ¿El servidor recupera el estado del cliente? ¿Se vuelven a unir las salas automáticamente?
- Usa AsyncAPI para proyectos grandes. El fichero YAML es la fuente de verdad que permite generar código cliente, tests y documentación visual.
- Versiona los eventos. Si cambias el schema de un evento, añade un sufijo de versión (
send_message_v2) o documenta la estrategia de migración.