Socket.IO
Overview
The @opra/socketio package provides SocketioAdapter — a platform adapter that integrates an OPRA ApiDocument into a Socket.IO server. The adapter registers all controller operations as socket event listeners, decodes incoming event arguments against your schema, dispatches to your handlers, and encodes responses — all driven by the type declarations in your schema.
Installation
npm install @opra/socketio socket.io
Setup
Create your ApiDocument with transport: 'ws', pass it to SocketioAdapter, then call listen() with an HTTP server or a port number.
import http from 'http';
import { SocketioAdapter } from '@opra/socketio';
import { ApiDocumentFactory } from '@opra/common';
import { ChatController } from './api/chat.controller.js';
const document = await ApiDocumentFactory.createDocument({
info: { title: 'Chat API', version: '1.0' },
api: {
transport: 'ws',
controllers: [ChatController],
},
});
const httpServer = http.createServer();
const adapter = new SocketioAdapter(document, {
scope: 'public',
});
adapter.listen(httpServer, { path: '/socket.io' });
httpServer.listen(3000, () => {
console.log('Listening on http://localhost:3000');
});
process.on('SIGTERM', async () => {
await adapter.close();
process.exit(0);
});
SocketioAdapter creates an internal socket.io.Server, registers all declared operations as event listeners, and attaches to the provided HTTP server. No manual socket.on() wiring is needed.
Adapter Options (SocketioAdapter.Options)
| Option | Type | Description |
|---|---|---|
scope | string | Validation scope applied during argument decoding and response encoding. |
interceptors | (InterceptorFunction | IWSInterceptor)[] | Interceptor chain executed on every incoming event. |
The underlying Socket.IO server options (e.g. path, cors, transports) are passed directly to listen():
adapter.listen(httpServer, {
path: '/socket.io',
cors: { origin: 'https://myapp.com' },
});
Defining Operations
Use @WSOperation() on a controller method to declare an operation. Each operation maps to a socket event name. Arguments are decoded positionally against the types declared in the decorator.
import { WSController, WSOperation, ApiField, ComplexType } from '@opra/common';
@ComplexType()
class MessageDto {
@ApiField({ required: true }) declare roomId: string;
@ApiField({ required: true }) declare text: string;
}
@WSController()
export class ChatController {
@WSOperation({ event: 'send-message', response: MessageDto })
async sendMessage(ctx: SocketioContext, payload: MessageDto) {
// payload is decoded and validated against MessageDto
return this.service.save(payload);
}
}
Event patterns
The event option accepts a string or a RegExp. When a RegExp is used, incoming event names are matched against the pattern:
@WSOperation({ event: /^room:.+/ })
async joinRoom(ctx: SocketioContext, roomId: string) { ... }
Default event name
If event is omitted, the operation name is used as the event name:
@WSOperation()
async ping(ctx: SocketioContext) {
return 'pong';
}
// listens for the 'ping' event
SocketioContext
Every operation handler receives a SocketioContext as its first argument.
import { SocketioContext } from '@opra/socketio';
async sendMessage(ctx: SocketioContext, payload: MessageDto) {
ctx.socket // the Socket.IO Socket instance
ctx.event // the incoming event name
ctx.parameters // decoded arguments array
ctx.server // the Socket.IO Server instance (for broadcasting)
}
| Property | Type | Description |
|---|---|---|
socket | socketio.Socket | The Socket.IO socket for the current connection. |
event | string | The name of the incoming event. |
parameters | any[] | Decoded and validated event arguments in declaration order. |
server | socketio.Server | The Socket.IO server — use for broadcasting to rooms or all clients. |
Broadcasting
Access ctx.server to broadcast to other clients:
async sendMessage(ctx: SocketioContext, payload: MessageDto) {
ctx.socket.to(payload.roomId).emit('new-message', payload);
return payload;
}
Request / Response
If the client sends a callback (acknowledgement), the handler's return value is automatically encoded and sent back as the acknowledgement payload. Errors are caught and returned as an OperationResult with an errors array — the client never receives an unhandled exception.
// Client side
socket.emit('send-message', payload, (response) => {
if (response.errors) console.error(response.errors);
else console.log(response.payload);
});
// Server side
@WSOperation({ event: 'send-message', response: MessageDto })
async sendMessage(ctx: SocketioContext, payload: MessageDto) {
return this.service.save(payload); // encoded as response.payload
}
Lifecycle
| Method | Description |
|---|---|
adapter.listen(srv, opts?) | Attaches to the given HTTP server or port and registers all event handlers. |
adapter.close() | Closes the Socket.IO server and clears all internal state. |
adapter.server | The underlying socketio.Server instance. |
Interceptors
Interceptors run before the operation handler on every incoming event.
import { SocketioContext } from '@opra/socketio';
const adapter = new SocketioAdapter(document, {
interceptors: [
async (ctx: SocketioContext, next) => {
console.log('Event:', ctx.event, 'from socket:', ctx.socket.id);
await next();
},
],
});
Class form:
import { IWSInterceptor, SocketioContext } from '@opra/socketio';
class AuthInterceptor implements IWSInterceptor {
async intercept(ctx: SocketioContext, next: () => Promise<any>) {
const token = ctx.socket.handshake.auth.token;
if (!token) throw new Error('Unauthorized');
await next();
}
}
const adapter = new SocketioAdapter(document, {
interceptors: [new AuthInterceptor()],
});
Error Handling
Throw any OpraException from a handler. If the client sent a callback, the error is serialized into the acknowledgement response as { errors: [...] }. If no callback was provided, the error is emitted on the adapter's error event.
import { OpraException } from '@opra/common';
async sendMessage(ctx: SocketioContext, payload: MessageDto) {
if (!payload.roomId) throw new OpraException('Missing roomId');
return this.service.save(payload);
}
Listen to adapter errors centrally:
adapter.on('error', (error, socket, adapter) => {
console.error('Socket.IO error:', error.message, socket?.id);
});
Events
| Event | Payload | Description |
|---|---|---|
connection | socketio.Socket, SocketioAdapter | Emitted when a new client connects. |
close | socketio.Socket, SocketioAdapter | Emitted when a client disconnects. |
execute | SocketioContext | Emitted just before the operation handler is called. |
error | Error, socketio.Socket | undefined, SocketioAdapter | Emitted when an error occurs. |