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.