Skip to content

WebSocket Gateways — thin transport, not business logic

The gateway is transport, not business logic. It receives a message, authorizes, hands off to a service.

nestjs-realtime-transport-thin-gateway

Why it matters

Failure modes if this rule is ignored
StakeIf ignored
Hard to test
  • Business logic in the gateway — can't hit it with HTTP tests or reuse it outside WebSockets.

How to fix

In NestJS, a WebSocket gateway has one responsibility. It does not share state with controllers, and business logic does not live inside it.

Examples

Bad
ts
@WebSocketGateway()
export class CollaborationGateway {
  private documents = new Map<string, Document>();   // ← state in gateway

  @SubscribeMessage('edit')
  handleEdit(@MessageBody() data: EditMessage) {
    const doc = this.documents.get(data.docId);
    doc.applyEdit(data.edit);
    // no way to test without opening a socket
  }
}
Good
ts
@Injectable()
export class CollaborationService {
  async applyEdit(docId: string, edit: Edit, userId: string): Promise<EditResult> {
    const doc = await this.repo.findById(docId);
    if (!this.policy.canEdit(user, doc)) throw new ForbiddenException();
    return doc.apply(edit);
  }
}

@WebSocketGateway()
export class CollaborationGateway {
  constructor(private collab: CollaborationService) {}

  @UseGuards(WsAuthGuard)
  @SubscribeMessage('edit')
  async handleEdit(@MessageBody() data: EditDto, @ConnectedSocket() client: AuthedSocket) {
    const result = await this.collab.applyEdit(data.docId, data.edit, client.userId);
    this.server.to(data.docId).emit('edit-applied', result);
  }
}

Contribute

Released under the MIT License.

esc