Socket.IO Module
The @opra/nestjs-socketio package integrates the OPRA Socket.IO adapter into a NestJS application. It discovers your OPRA WebSocket controllers automatically from the NestJS provider tree, wires up the Socket.IO server lifecycle through NestJS hooks, and resolves class-based interceptors through the NestJS DI container — all transparently.
Installation
npm install @opra/nestjs-socketio @opra/socketio @opra/common
Setup
Import OpraSocketioModule in your root application module:
import { Module } from '@nestjs/common';
import { OpraSocketioModule } from '@opra/nestjs-socketio';
import { ChatController } from './chat/chat.controller.js';
import { ChatService } from './chat/chat.service.js';
import * as models from './models/models.js';
@Module({
imports: [
OpraSocketioModule.forRoot({
imports: [DatabaseModule],
providers: [ChatController, ChatService],
name: 'ChatApi',
info: { title: 'Chat API', version: '1.0' },
types: [...Object.values(models)],
schemaIsPublic: true,
}),
],
})
export class AppModule {}
Unlike the HTTP module, you do not pass a controllers list — OPRA discovers all providers decorated with @WSController automatically from the NestJS module context.
Options
| Option | Type | Description |
|---|---|---|
name | string | API name. |
info | object | Document metadata — title, version, description. |
types | any[] | Data types to register (decorated classes, EnumType results). |
references | Record<string, ReferenceThunk> | Namespaced references to other ApiDocument instances or async thunks. |
port | number | Port for the Socket.IO server. If omitted or equal to the HTTP server port, Socket.IO is attached to the existing HTTP server. If different, a separate server is started. |
serverOptions | Partial<socketio.ServerOptions> | Options passed directly to the Socket.IO server (e.g. path, cors, transports). |
scope | string | Validation scope applied to every incoming event and outgoing response. |
interceptors | (InterceptorFunction | IWSInterceptor | Type<IWSInterceptor>)[] | Interceptor chain executed on every event. Class types are resolved through the NestJS DI container. |
imports | any[] | NestJS modules to import into the OPRA module context. |
providers | Provider[] | NestJS providers available for injection inside controllers. |
exports | any[] | Providers to export from the OPRA module to the rest of the application. |
global | boolean | Register the module as a NestJS global module. |
Async configuration
Use forRootAsync() when info, types, or other options depend on injected services:
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { OpraSocketioModule } from '@opra/nestjs-socketio';
import * as models from './models/models.js';
@Module({
imports: [
ConfigModule.forRoot(),
OpraSocketioModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
providers: [ChatController, ChatService],
useFactory: (config: ConfigService) => ({
name: 'ChatApi',
info: {
title: 'Chat API',
version: config.get('API_VERSION'),
},
types: [...Object.values(models)],
port: config.get('WS_PORT'),
}),
}),
],
})
export class AppModule {}
How it works
OpraSocketioModule scans all NestJS providers in its module context for classes decorated with @WSController and builds the ApiDocument from them automatically. No explicit controller list is required.
Once the document is ready, the module creates a SocketioAdapter and attaches it to the application during onApplicationBootstrap:
- If
portis omitted or matches the HTTP server port, Socket.IO is attached to the existing HTTP server — no extra port needed. - If
portis set to a different value, a separate HTTP server is started and Socket.IO listens on it.
The adapter is cleanly shut down during onApplicationShutdown.
Dependency injection in controllers
OPRA WebSocket controllers are registered as NestJS providers, so you can inject services directly into them:
import { WSController, WSOperation } from '@opra/common';
import { SocketioContext } from '@opra/socketio';
import { Injectable } from '@nestjs/common';
import { ChatService } from './chat.service.js';
@Injectable()
@WSController()
export class ChatController {
constructor(private readonly service: ChatService) {}
@WSOperation({ event: 'send-message' })
async sendMessage(ctx: SocketioContext, payload: MessageDto) {
return this.service.save(payload);
}
}
Register the controller as a NestJS provider — either in providers of the module options or in any imported module:
OpraSocketioModule.forRoot({
providers: [ChatController, ChatService],
// ...
})
Guards and interceptors
NestJS guards and interceptors work normally on OPRA WebSocket controllers. Apply them at the class or method level:
import { UseGuards } from '@nestjs/common';
import { AuthGuard } from './auth.guard.js';
@UseGuards(AuthGuard)
@WSController()
export class ChatController { ... }
OPRA-level interceptors (the interceptors option) run on every incoming event before the operation handler. Class-based interceptors are resolved through the NestJS DI container, so they can have injected dependencies:
import { IWSInterceptor, SocketioContext } from '@opra/socketio';
import { Injectable } from '@nestjs/common';
import { LogService } from './log.service.js';
@Injectable()
class LoggingInterceptor implements IWSInterceptor {
constructor(private readonly log: LogService) {}
async intercept(ctx: SocketioContext, next: () => Promise<any>) {
this.log.info(`Event: ${ctx.event} socket=${ctx.socket.id}`);
await next();
}
}
OpraSocketioModule.forRoot({
providers: [ChatController, ChatService, LogService, LoggingInterceptor],
interceptors: [LoggingInterceptor], // resolved via DI
// ...
})
Separate Socket.IO port
By default Socket.IO shares the same port as the NestJS HTTP server. To run Socket.IO on a dedicated port, set the port option:
OpraSocketioModule.forRoot({
port: 3001,
serverOptions: {
cors: { origin: 'https://myapp.com' },
},
// ...
})
The NestJS HTTP server continues to serve REST routes on its own port; Socket.IO listens independently on port 3001.