API

Complete API reference for all WebSocket decorators

This page provides a complete reference for all decorators available in the @vercube/ws module. Each decorator is explained with its purpose, parameters, behavior, and practical examples.

@Namespace

The @Namespace decorator defines a WebSocket namespace path and must be applied to a controller class.

Signature

function Namespace(path: string): ClassDecorator

Parameters

ParameterTypeRequiredDescription
pathstringYesThe namespace path (URL path) for WebSocket connections

Behavior

  • Registers the namespace path in the WebSocket service
  • Clients connect to ws://yourserver.com{path}
  • All message handlers in the controller are scoped to this namespace
  • Must be used on the same class as @Controller()

Examples

@Namespace('/chat')
@Controller()
export class ChatGateway {
  // All handlers are under /chat namespace
}

// Clients connect to: ws://yourserver.com/chat

Rules

  • Path is case-insensitive (internally normalized to lowercase)
  • Leading slash is recommended but not required
  • Each namespace is isolated - messages don't cross namespace boundaries
  • You cannot have duplicate namespace paths

Common Mistakes

// ❌ Missing @Namespace
@Controller()
export class BadGateway {
  @Message({ event: 'test' })
  handleTest() { ... }
}
// Warning: Unable to find namespace. Did you use @Namespace()?

// ❌ Empty namespace
@Namespace('')
@Controller()
export class BadGateway { ... }

// ✅ Correct
@Namespace('/my-namespace')
@Controller()
export class GoodGateway { ... }

@OnConnectionAttempt

The @OnConnectionAttempt decorator handles WebSocket connection attempts, allowing you to accept or reject connections based on authentication, authorization, or other criteria.

Signature

function OnConnectionAttempt(): MethodDecorator

Parameters

This decorator takes no parameters.

Handler Method Signature

async (
  params: Record<string, unknown>,
  request: Request
): Promise<boolean | void>
ParameterTypeDescription
paramsRecord<string, unknown>Query parameters from the connection URL
requestRequestThe original HTTP upgrade request

Behavior

  • Called before a WebSocket connection is established
  • Return true or undefined to accept the connection
  • Return false to reject with 403 Forbidden
  • Throw an error to reject with 403 and error message
  • Optional - if not defined, all connections are accepted
  • Only one @OnConnectionAttempt handler per namespace

Examples

@Namespace('/secure')
@Controller()
export class SecureGateway {
  
  @OnConnectionAttempt()
  async validateConnection(
    params: Record<string, unknown>,
    request: Request
  ): Promise<boolean> {
    const token = params.token as string;
    
    if (!token) {
      return false; // Reject
    }
    
    return true; // Accept
  }
}

// Client: ws://server.com/secure?token=abc123

Return Values

Return ValueBehavior
trueAccept connection
falseReject with 403 Forbidden
undefined (void)Accept connection
Thrown errorReject with 403 and error message

Common Patterns

@OnConnectionAttempt()
async validateConnection(params: Record<string, unknown>): Promise<boolean> {
  // All async operations work
  const isValid = await this.database.checkUser(params.userId);
  const hasPermission = await this.checkPermissions(params.userId);
  
  return isValid && hasPermission;
}

@Message

The @Message decorator listens for incoming WebSocket messages with a specific event name.

Signature

function Message(options: MessageDecoratorOptions): MethodDecorator

interface MessageDecoratorOptions {
  event: string;
  validationSchema?: ValidationTypes.Schema;
}

Parameters

ParameterTypeRequiredDescription
eventstringYesThe event name to listen for
validationSchemaSchemaNoZod schema for validating incoming message data

Handler Method Signature

async (
  data: unknown,
  peer: Peer
): Promise<any>
ParameterTypeDescription
dataunknown (or typed with schema)The message data sent by the client
peerPeerThe connected peer that sent the message

Peer Object

interface Peer {
  id: string;          // Unique peer identifier
  namespace?: string;  // Namespace the peer is connected to
  ip?: string;        // Client IP address
  send(message: unknown): void;  // Send message to this peer
}

Behavior

  • Listens for messages with { event: '...', data: {...} } structure
  • Can have multiple message handlers with different events
  • Supports schema validation with Zod or other validators
  • Handler return value can be used with @Emit, @Broadcast, etc.
  • Invalid messages (validation errors) are logged and ignored

Examples

@Namespace('/chat')
@Controller()
export class ChatGateway {
  
  @Message({ event: 'send-message' })
  handleMessage(data: any, peer: Peer) {
    console.log(`Message from ${peer.id}:`, data);
  }
}

// Client sends:
// ws.send(JSON.stringify({
//   event: 'send-message',
//   data: { text: 'Hello!' }
// }));

Validation Error Handling

When validation fails, the message is rejected and error details are logged:

// Client sends invalid data:
ws.send(JSON.stringify({
  event: 'send-message',
  data: { text: '' }  // Too short!
}));

// Server logs:
// "Websocket message validation error"
// [{ path: 'text', message: 'String must contain at least 1 character(s)' }]

Common Patterns

@Namespace('/game')
@Controller()
export class GameGateway {
  private playerStates = new Map<string, PlayerState>();
  
  @Message({ event: 'player-move' })
  handleMove(data: { x: number; y: number }, peer: Peer) {
    // Update player state
    const state = this.playerStates.get(peer.id) || this.createInitialState();
    state.position = { x: data.x, y: data.y };
    this.playerStates.set(peer.id, state);
  }
}

@Emit

The @Emit decorator sends the handler's return value back to the client that sent the message.

Signature

function Emit(event: string): MethodDecorator

Parameters

ParameterTypeRequiredDescription
eventstringYesThe event name for the emitted message

Behavior

  • Sends message only to the peer that triggered the handler
  • Uses the handler's return value as message data
  • Must be used with @Message()
  • Executes after the handler completes
  • Automatically wraps data in { event: '...', data: {...} } format

Examples

@Namespace('/chat')
@Controller()
export class ChatGateway {
  
  @Message({ event: 'ping' })
  @Emit('pong')
  handlePing() {
    return { timestamp: Date.now() };
  }
}

// Client sends: { event: 'ping', data: {} }
// Client receives: { event: 'pong', data: { timestamp: 1234567890 } }

Return Value Requirements

The handler must return a value (or Promise that resolves to a value):

// ✅ Good - Returns value
@Message({ event: 'test' })
@Emit('result')
handleTest() {
  return { foo: 'bar' };
}

// ✅ Good - Async return
@Message({ event: 'test' })
@Emit('result')
async handleTest() {
  const data = await this.fetchData();
  return data;
}

// ⚠️ Warning - Returns void
@Message({ event: 'test' })
@Emit('result')
handleTest() {
  // No return - client receives empty data
}

@Broadcast

The @Broadcast decorator sends the handler's return value to all connected peers in the namespace, including the sender.

Signature

function Broadcast(event: string): MethodDecorator

Parameters

ParameterTypeRequiredDescription
eventstringYesThe event name for the broadcasted message

Behavior

  • Sends message to ALL peers in the namespace
  • Includes the peer that triggered the handler
  • Uses the handler's return value as message data
  • Must be used with @Message()
  • All peers receive the same message simultaneously

Examples

@Namespace('/chat')
@Controller()
export class ChatGateway {
  
  @Message({ event: 'user-message' })
  @Broadcast('new-message')
  handleMessage(data: { text: string; user: string }) {
    return {
      text: data.text,
      user: data.user,
      timestamp: Date.now()
    };
  }
}

// When user A sends a message:
// - User A receives it
// - User B receives it
// - User C receives it
// ... all users receive the same message

Broadcast vs Emit

// @Emit - Only sender receives response
@Message({ event: 'ping' })
@Emit('pong')
handlePing() { ... }

// @Broadcast - Everyone receives (including sender)
@Message({ event: 'announce' })
@Broadcast('announcement')
handleAnnounce() { ... }

Common Patterns

@Message({ 
  event: 'post-message',
  validationSchema: MessageSchema 
})
@Broadcast('new-post')
async handlePost(data: MessageDto) {
  // Save to database
  await this.messageService.save(data);
  
  // Broadcast to all
  return {
    id: generateId(),
    ...data,
    createdAt: new Date()
  };
}

@BroadcastOthers

The @BroadcastOthers decorator sends the handler's return value to all connected peers in the namespace, excluding the sender.

Signature

function BroadcastOthers(event: string): MethodDecorator

Parameters

ParameterTypeRequiredDescription
eventstringYesThe event name for the broadcasted message

Behavior

  • Sends message to ALL peers in the namespace EXCEPT the sender
  • Uses the handler's return value as message data
  • Must be used with @Message()
  • Sender doesn't receive the broadcast (use @Emit if needed)

Examples

@Namespace('/chat')
@Controller()
export class ChatGateway {
  
  @Message({ event: 'typing' })
  @BroadcastOthers('user-typing')
  handleTyping(data: { username: string; isTyping: boolean }) {
    return {
      username: data.username,
      isTyping: data.isTyping
    };
  }
}

// User A types:
// - User A: doesn't see their own typing indicator
// - User B: sees "User A is typing..."
// - User C: sees "User A is typing..."

Combining with @Emit

You can use both decorators to send different messages:

@Message({ event: 'action' })
@Emit('action-confirmed')          // Sender gets confirmation
@BroadcastOthers('user-action')    // Others get notification
handleAction(data: any, peer: Peer) {
  return {
    action: data.action,
    userId: peer.id
  };
}

// Sender receives: { event: 'action-confirmed', data: {...} }
// Others receive: { event: 'user-action', data: {...} }

Common Patterns

@Message({ event: 'join-room' })
@BroadcastOthers('user-joined')
handleJoinRoom(data: { roomId: string; username: string }) {
  // Add user to room
  this.addToRoom(data.roomId, data.username);
  
  // Notify others
  return {
    roomId: data.roomId,
    username: data.username,
    message: `${data.username} joined the room`
  };
}

Decorator Combinations

You can combine decorators to create powerful patterns:

@Message + @Emit

Send response back to sender only:

@Message({ event: 'request' })
@Emit('response')
handleRequest(data: any) {
  return { result: 'processed' };
}

@Message + @Broadcast

Notify everyone including sender:

@Message({ event: 'update' })
@Broadcast('updated')
handleUpdate(data: any) {
  return { updated: true };
}

@Message + @BroadcastOthers

Notify everyone except sender:

@Message({ event: 'action' })
@BroadcastOthers('notification')
handleAction(data: any) {
  return { action: 'completed' };
}

@Message + @Emit + @BroadcastOthers

Send different messages to sender and others:

@Message({ event: 'post' })
@Emit('post-confirmed')
@BroadcastOthers('new-post')
handlePost(data: PostDto) {
  return data;
}
// Sender: { event: 'post-confirmed', data: {...} }
// Others: { event: 'new-post', data: {...} }

Multiple Handlers Same Namespace

@Namespace('/app')
@Controller()
export class AppGateway {
  
  @Message({ event: 'ping' })
  @Emit('pong')
  handlePing() {
    return { time: Date.now() };
  }
  
  @Message({ event: 'broadcast' })
  @Broadcast('message')
  handleBroadcast(data: any) {
    return data;
  }
  
  @Message({ event: 'notify-others' })
  @BroadcastOthers('notification')
  handleNotify(data: any) {
    return data;
  }
}

Event Naming

// ✅ Good - Descriptive, namespaced
@Message({ event: 'chat:send-message' })
@Message({ event: 'game:player-move' })
@Message({ event: 'document:edit' })

// ❌ Bad - Generic, unclear
@Message({ event: 'message' })
@Message({ event: 'update' })
@Message({ event: 'data' })

Type Safety

// ✅ Good - Typed with schema
const Schema = z.object({ ... });
type Data = z.infer<typeof Schema>;

@Message({ 
  event: 'action',
  validationSchema: Schema 
})
handleAction(data: Data, peer: Peer) { ... }

// ❌ Bad - Untyped
@Message({ event: 'action' })
handleAction(data: any, peer: Peer) { ... }

Error Handling

// ✅ Good - Graceful error handling
@Message({ event: 'risky' })
async handleRisky(data: any) {
  try {
    return await this.process(data);
  } catch (error) {
    return { error: error.message };
  }
}

// ❌ Bad - Unhandled errors
@Message({ event: 'risky' })
async handleRisky(data: any) {
  return await this.process(data); // May throw
}

Single Responsibility

// ✅ Good - One concern per handler
@Message({ event: 'create-order' })
async createOrder(data: OrderDto) { ... }

@Message({ event: 'cancel-order' })
async cancelOrder(data: { orderId: string }) { ... }

// ❌ Bad - Multiple concerns
@Message({ event: 'order-action' })
async handleOrder(data: any) {
  if (data.action === 'create') { ... }
  else if (data.action === 'cancel') { ... }
}
Previous

MCP

Expose AI tools with Model Context Protocol

Next