mcpField
N
nestjs-websocket

nestjs-websocket

github.com/claude-dev-suite開発4646 回インストール2 日前に更新
リポジトリ

nestjs-websocket について

NestJSアプリケーションにWebSocket機能を追加するためのリファレンススキルです。ゲートウェイ設定、認証、ルーム管理、エラー処理、スケーリングの指針を扱います。

主な機能

  1. 01WebSocketゲートウェイの設定
  2. 02モジュール登録
  3. 03JWT認証ガード
  4. 04ルームとイベントの処理
  5. 05Redisアダプターによるスケーリング

活用シーン

  1. 01NestJSアプリへのリアルタイムイベント追加
  2. 02WebSocket接続の認証
  3. 03ルームへのメッセージ配信
  4. 04WebSocketサービスの水平スケーリング準備

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/claude-dev-suite/claude-dev-suite --skill nestjs-websocket

インストール後、Claude Code で/nestjs-websocketを入力して読み込まれたか確認します。

詳細

NestJS WebSocket 統合

深い知識:包括的なドキュメントを参照するには、technology: nestjs を指定して mcp__documentation__fetch_docs を使用してください。

ゲートウェイのセットアップ

// events.gateway.ts
import {
  WebSocketGateway,
  WebSocketServer,
  SubscribeMessage,
  OnGatewayConnection,
  OnGatewayDisconnect,
  ConnectedSocket,
  MessageBody,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';

@WebSocketGateway({
  cors: {
    origin: process.env.CORS_ORIGINS?.split(',') || [],
    credentials: true,
  },
  namespace: '/events',
})
export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
  @WebSocketServer()
  server: Server;

  handleConnection(client: Socket) {
    console.log(`Client connected: ${client.id}`);
  }

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

  @SubscribeMessage('message')
  handleMessage(
    @ConnectedSocket() client: Socket,
    @MessageBody() payload: { room: string; data: any },
  ) {
    this.server.to(payload.room).emit('message', payload.data);
    return { event: 'message', data: 'Message sent' };
  }
}

モジュールの登録

// events.module.ts
import { Module } from '@nestjs/common';
import { EventsGateway } from './events.gateway';

@Module({
  providers: [EventsGateway],
  exports: [EventsGateway],
})
export class EventsModule {}

// app.module.ts
@Module({
  imports: [EventsModule],
})
export class AppModule {}

認証ガード

// ws-auth.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { WsException } from '@nestjs/websockets';
import { Socket } from 'socket.io';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class WsAuthGuard implements CanActivate {
  constructor(private jwtService: JwtService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const client: Socket = context.switchToWs().getClient();
    const token = client.handshake.auth?.token ||
                  client.handshake.headers?.authorization?.split(' ')[1];

    if (!token) {
      throw new WsException('Unauthorized');
    }

    try {
      const payload = await this.jwtService.verifyAsync(token);
      client.data.user = payload;
      return true;
    } catch {
      throw new WsException('Invalid token');
    }
  }
}

// Usage in gateway
@UseGuards(WsAuthGuard)
@SubscribeMessage('protected-event')
handleProtectedEvent(@ConnectedSocket() client: Socket) {
  const user = client.data.user;
  return { user };
}

ルーム管理

@WebSocketGateway()
export class RoomsGateway {
  @WebSocketServer()
  server: Server;

  @SubscribeMessage('join-room')
  handleJoinRoom(
    @ConnectedSocket() client: Socket,
    @MessageBody() roomId: string,
  ) {
    client.join(roomId);
    client.to(roomId).emit('user-joined', { id: client.id });
    return { event: 'joined', room: roomId };
  }

  @SubscribeMessage('leave-room')
  handleLeaveRoom(
    @ConnectedSocket() client: Socket,
    @MessageBody() roomId: string,
  ) {
    client.leave(roomId);
    client.to(roomId).emit('user-left', { id: client.id });
  }

  // Broadcast to room from service
  broadcastToRoom(roomId: string, event: string, data: any) {
    this.server.to(roomId).emit(event, data);
  }

  // Get clients in room
  async getClientsInRoom(roomId: string): Promise<string[]> {
    const sockets = await this.server.in(roomId).fetchSockets();
    return sockets.map((s) => s.id);
  }
}

例外処理

// ws-exception.filter.ts
import { Catch, ArgumentsHost } from '@nestjs/common';
import { BaseWsExceptionFilter, WsException } from '@nestjs/websockets';
import { Socket } from 'socket.io';

@Catch()
export class WsExceptionFilter extends BaseWsExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const client: Socket = host.switchToWs().getClient();

    const error = exception instanceof WsException
      ? exception.getError()
      : { message: 'Internal server error' };

    client.emit('error', {
      status: 'error',
      message: typeof error === 'string' ? error : (error as any).message,
    });
  }
}

// Apply globally or per gateway
@UseFilters(new WsExceptionFilter())
@WebSocketGateway()
export class EventsGateway {}

Redis アダプターによるスケーリング

// main.ts
import { IoAdapter } from '@nestjs/platform-socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';

export class RedisIoAdapter extends IoAdapter {
  private adapterConstructor: ReturnType<typeof createAdapter>;

  async connectToRedis(): Promise<void> {
    const pubClient = createClient({ url: process.env.REDIS_URL });
    const subClient = pubClient.duplicate();

    await Promise.all([pubClient.connect(), subClient.connect()]);

    this.adapterConstructor = createAdapter(pubClient, subClient);
  }

  createIOServer(port: number, options?: any) {
    const server = super.createIOServer(port, options);
    server.adapter(this.adapterConstructor);
    return server;
  }
}

// Bootstrap
async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const redisIoAdapter = new RedisIoAdapter(app);
  await redisIoAdapter.connectToRedis();
  app.useWebSocketAdapter(redisIoAdapter);

  await app.listen(3000);
}

イベントリスナー

@Component
@RequiredArgsConstructor
@Slf4j
export class WebSocketEventListener {
  constructor(private messagingTemplate: SimpMessagingTemplate) {}

  @EventListener
  handleWebSocketConnectListener(event: SessionConnectedEvent) {
    log.info('New WebSocket connection established');
  }

  @EventListener
  handleWebSocketDisconnectListener(event: SessionDisconnectEvent) {
    const headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
    const username = headerAccessor.getSessionAttributes().get('username');

    if (username != null) {
      log.info(`User disconnected: ${username}`);
      const leaveMessage = ChatMessage.builder()
        .type(ChatMessage.MessageType.LEAVE)
        .sender(username)
        .build();
      messagingTemplate.convertAndSend('/topic/public', leaveMessage);
    }
  }
}

ゲートウェイをサービスに注入する

// notifications.service.ts
@Injectable()
export class NotificationsService {
  constructor(private eventsGateway: EventsGateway) {}

  async notifyUser(userId: string, notification: Notification) {
    this.eventsGateway.server
      .to(`user:${userId}`)
      .emit('notification', notification);
  }

  async broadcastAnnouncement(message: string) {
    this.eventsGateway.server.emit('announcement', { message });
  }
}

アンチパターン

アンチパターン問題点適切な方法
名前空間を使用しないすべてのイベントが混在します名前空間を使用して分離します
接続時の認証がない不正アクセスにつながりますWsAuthGuard を実装します
ルームのクリーンアップがないメモリーリークが発生します切断イベントを処理します
ハンドラー内で重い処理を同期実行するイベントループをブロックしますasync/await を適切に使用します
本番環境に Redis アダプターがない水平スケーリングできませんRedis アダプターを使用します

トラブルシューティングのヒント

症状原因解決方法
CORS エラーCORS 設定がありませんゲートウェイに cors オプションを追加します
認証が常に失敗するトークンがハンドシェイクに含まれていませんクライアントの認証設定を確認します
メッセージが受信されない名前空間が間違っています名前空間が一致していることを確認します
ルームへのブロードキャストに失敗するクライアントがルームに参加していませんjoin-room が呼び出されたことを確認します
スケーリングの問題Redis アダプターがありませんRedisIoAdapter を実装します

本番環境チェックリスト

  • CORS が適切に設定されている
  • JWT 認証ガードがある
  • 水平スケーリング用の Redis アダプターがある
  • エラー処理用の例外フィルターがある
  • 切断が適切に処理される
  • メッセージのレート制限がある
  • ハートビート/ping が設定されている
  • 接続/切断のログ記録がある

このスキルを使用しない場合

  • React WebSocket クライアントの場合 → react-websocket を使用します
  • Spring WebSocket の場合 → spring-websocket を使用します
  • 生の HTTP ロングポーリングの場合 → 標準のコントローラーを使用します

リファレンスドキュメント

コメント