Skip to content

WebSocket

Contract

@modularityjs/ws provides a decorator-based WebSocket gateway system with typed message handling, connection lifecycle hooks, and parameter injection.

Gateways

Gateways are classes that handle WebSocket connections on a specific path. The mounted URL is the configured WsConfig.path prefix (default /ws) concatenated with the gateway path — so @WsGateway('/chat') with the default config is served at /ws/chat, not /chat.

typescript
import {
  Client,
  Data,
  OnConnect,
  OnDisconnect,
  OnMessage,
  WsGateway,
  WsGatewaysPool,
} from '@modularityjs/ws';
import type { WsClient } from '@modularityjs/ws';

@WsGateway('/chat') // served at '/ws/chat' with default WsConfig.path
class ChatGateway {
  @OnConnect()
  handleConnect(@Client() client: WsClient) {
    console.log(`Client ${client.id} connected`);
  }

  @OnDisconnect()
  handleDisconnect(@Client() client: WsClient) {
    console.log(`Client ${client.id} disconnected`);
  }

  @OnMessage('chat:send')
  handleMessage(@Client() client: WsClient, @Data() data: { text: string }) {
    console.log(`Message from ${client.id}: ${data.text}`);
  }
}

Register gateways via the WsGatewaysPool:

typescript
@Module({
  name: 'chat',
  imports: [WsModule],
  providers: [ChatGateway],
  pools: [
    {
      pool: WsGatewaysPool,
      key: 'chat-gateway',
      useClass: ChatGateway,
    },
  ],
})
class ChatModule {}

Decorators

Class Decorators

DecoratorDescription
@WsGateway(path)Marks a class as a WebSocket gateway. The mounted URL is WsConfig.path + path (default prefix /ws, so /ws + path).

Method Decorators

DecoratorDescription
@OnConnect()Called when a client connects
@OnDisconnect()Called when a client disconnects
@OnMessage(type)Called when a message with the given type arrives

Parameter Decorators

DecoratorDescription
@Client()Injects the WsClient instance
@Data()Injects the message data payload

WsClient

Each connected client exposes:

typescript
interface WsClient {
  readonly id: string;
  send(data: unknown): void;
  close(code?: number, reason?: string): void;
}

WsMessage

Messages exchanged over the WebSocket follow a typed envelope:

typescript
interface WsMessage {
  readonly type: string;
  readonly data: unknown;
}

WsServer

The abstract WsServer provides server-wide operations:

typescript
abstract class WsServer {
  abstract broadcast(type: string, data: unknown): void;
  abstract getClients(): ReadonlySet<WsClient>;
  abstract close(): Promise<void>;
}

Inject WsServer to broadcast messages to all connected clients:

typescript
import { Inject, Injectable } from '@modularityjs/di';
import { WsServer } from '@modularityjs/ws';

@Injectable()
class NotificationService {
  constructor(@Inject(WsServer) private readonly ws: WsServer) {}

  notifyAll(message: string): void {
    this.ws.broadcast('notification', { message });
  }

  getOnlineCount(): number {
    return this.ws.getClients().size;
  }
}

Drivers

Fastify (@modularityjs/ws-fastify)

Integrates with the existing Fastify HTTP server via @fastify/websocket. Requires @modularityjs/http-fastify.

typescript
import { HttpModule } from '@modularityjs/http';
import { WsModule } from '@modularityjs/ws';
import { HttpFastifyModule } from '@modularityjs/http-fastify';
import { WsFastifyModule } from '@modularityjs/ws-fastify';

const modules = [
  HttpModule.forRoot({ port: 3000 }),
  HttpFastifyModule,
  WsModule,
  WsFastifyModule,
];

The driver registers the @fastify/websocket plugin on the Fastify instance, then walks every @WsGateway-decorated class and wires up routes and message handlers for the ones whose providers are bound in the DI container (i.e. registered via WsGatewaysPool). On shutdown, all client connections are closed gracefully.

Configuration

typescript
WsModule.forRoot({
  path: '/ws', // URL prefix prepended to every gateway path (default '/ws')
  maxClients: 1000, // hard cap on concurrent client sockets (default)
  idleTimeoutMs: undefined, // close sockets idle longer than this; unset = no timeout
});

path is a prefix, not a literal route. A gateway declared as @WsGateway('/chat') is served at path + '/chat' — so '/ws/chat' with the default, or '/chat' if path: ''.

Usage Example

A chat gateway that broadcasts messages to all connected clients (served at /ws/chat with the default WsConfig.path = '/ws'):

typescript
@WsGateway('/chat')
class ChatGateway {
  constructor(@Inject(WsServer) private readonly server: WsServer) {}

  @OnConnect()
  handleConnect(@Client() client: WsClient) {
    this.server.broadcast('system', {
      text: `User ${client.id} joined`,
    });
  }

  @OnMessage('chat:send')
  handleMessage(@Client() client: WsClient, @Data() data: { text: string }) {
    this.server.broadcast('chat:message', {
      from: client.id,
      text: data.text,
    });
  }

  @OnDisconnect()
  handleDisconnect(@Client() client: WsClient) {
    this.server.broadcast('system', {
      text: `User ${client.id} left`,
    });
  }
}

Client-side messages use the { type, data } envelope:

json
{ "type": "chat:send", "data": { "text": "Hello, world!" } }