Skip to main content

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)​

OptionTypeDescription
scopestringValidation 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)
}
PropertyTypeDescription
socketsocketio.SocketThe Socket.IO socket for the current connection.
eventstringThe name of the incoming event.
parametersany[]Decoded and validated event arguments in declaration order.
serversocketio.ServerThe 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​

MethodDescription
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.serverThe 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​

EventPayloadDescription
connectionsocketio.Socket, SocketioAdapterEmitted when a new client connects.
closesocketio.Socket, SocketioAdapterEmitted when a client disconnects.
executeSocketioContextEmitted just before the operation handler is called.
errorError, socketio.Socket | undefined, SocketioAdapterEmitted when an error occurs.