NestJS และ WebSockets ในปี 2026: Real-Time, Gateway และคำถามสัมภาษณ์งาน

คู่มือครบถ้วนสำหรับการสร้างแอปพลิเคชัน real-time ด้วย NestJS และ WebSockets รวมถึงการใช้งาน gateway, best practices และคำถามสัมภาษณ์ทางเทคนิค

NestJS WebSockets Real-Time Gateway

การสื่อสารแบบ real-time กลายเป็นความต้องการพื้นฐานในการพัฒนาแอปพลิเคชันสมัยใหม่ ตั้งแต่แอปพลิเคชันแชท การแจ้งเตือนทันที ไปจนถึงแดชบอร์ดสำหรับการตรวจสอบ WebSockets นำเสนอโซลูชันที่มีประสิทธิภาพสำหรับการสื่อสารสองทางระหว่าง client และ server NestJS ในฐานะ framework Node.js ที่ทรงพลัง มอบ abstraction ที่สง่างามสำหรับการทำงานกับ WebSockets ผ่านแนวคิดของ Gateway

บทความนี้นำเสนอการใช้งาน WebSockets ใน NestJS อย่างละเอียด ตั้งแต่แนวคิดพื้นฐานไปจนถึง pattern ขั้นสูงที่ใช้ใน production เนื้อหายังครอบคลุมคำถามสัมภาษณ์ทางเทคนิคที่พบบ่อยเกี่ยวกับหัวข้อนี้

NestJS 11 นำเสนอการปรับปรุงประสิทธิภาพที่สำคัญสำหรับ WebSocket adapter รวมถึงการรองรับ native สำหรับ binary messages และ compression ควรใช้เวอร์ชันล่าสุดเพื่อใช้ประโยชน์จากฟีเจอร์ที่เหมาะสมที่สุด

ทำความเข้าใจ WebSocket Gateway ใน NestJS

Gateway คือ class ที่ถูก decorate ด้วย @WebSocketGateway() และทำหน้าที่เป็นจุดเข้าสำหรับการเชื่อมต่อ WebSocket แตกต่างจาก HTTP controller ทั่วไป gateway อนุญาตให้มีการสื่อสารสองทางอย่างต่อเนื่องระหว่าง client และ server

แนวคิดของ gateway ใน NestJS ได้รับแรงบันดาลใจจาก pattern ที่คล้ายกันใน framework อื่น แต่รวมเข้ากับระบบ dependency injection ของ NestJS อย่างเต็มที่ ทำให้สามารถใช้ service, guard, interceptor และ pipe เดียวกันกับ HTTP controller ได้

typescript
import {
  WebSocketGateway,
  WebSocketServer,
  SubscribeMessage,
  MessageBody,
  ConnectedSocket,
  OnGatewayInit,
  OnGatewayConnection,
  OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { Logger } from '@nestjs/common';

@WebSocketGateway({
  cors: {
    origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
    credentials: true,
  },
  namespace: '/events',
  transports: ['websocket', 'polling'],
})
export class EventsGateway
  implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
{
  @WebSocketServer()
  server: Server;

  private readonly logger = new Logger(EventsGateway.name);

  afterInit(server: Server): void {
    this.logger.log('WebSocket Gateway initialized');
  }

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

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

  @SubscribeMessage('message')
  handleMessage(
    @MessageBody() data: { content: string; room?: string },
    @ConnectedSocket() client: Socket,
  ): { event: string; data: unknown } {
    if (data.room) {
      client.to(data.room).emit('message', data);
    }
    return { event: 'message', data: { received: true, timestamp: Date.now() } };
  }
}

โค้ดด้านบนแสดงการใช้งาน gateway พื้นฐานพร้อม lifecycle hooks เมธอด afterInit, handleConnection และ handleDisconnect ให้การควบคุมเต็มที่ต่อวงจรชีวิตของการเชื่อมต่อ WebSocket

การกำหนดค่า Adapter: Socket.IO และ ws

NestJS รองรับสอง adapter หลักสำหรับ WebSocket: Socket.IO และ ws การเลือก adapter ขึ้นอยู่กับความต้องการเฉพาะของแอปพลิเคชัน

Socket.IO มีฟีเจอร์เพิ่มเติม เช่น automatic reconnection, room management และ fallback ไปยัง polling ส่วน adapter ws เบากว่าและเหมาะสำหรับสถานการณ์ที่ต้องการประสิทธิภาพสูงสุดพร้อม overhead ขั้นต่ำ

typescript
import { NestFactory } from '@nestjs/core';
import { IoAdapter } from '@nestjs/platform-socket.io';
import { WsAdapter } from '@nestjs/platform-ws';
import { AppModule } from './app.module';

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

  // ใช้ Socket.IO adapter
  app.useWebSocketAdapter(new IoAdapter(app));

  // หรือใช้ ws adapter
  // app.useWebSocketAdapter(new WsAdapter(app));

  await app.listen(3000);
}
bootstrap();

สำหรับแอปพลิเคชันที่ต้องการความสามารถในการ scale แนวนอน Redis adapter เป็นตัวเลือกที่เหมาะสม adapter นี้ช่วยให้หลาย instance ของ server แชร์ state การเชื่อมต่อได้

typescript
import { IoAdapter } from '@nestjs/platform-socket.io';
import { ServerOptions } from 'socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';
import { INestApplication } from '@nestjs/common';

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

  async connectToRedis(): Promise<void> {
    const pubClient = createClient({
      url: process.env.REDIS_URL || 'redis://localhost:6379',
    });
    const subClient = pubClient.duplicate();

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

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

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

// ใน main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);

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

  await app.listen(3000);
}

การยืนยันตัวตนและการอนุญาตสิทธิ์ WebSocket

ความปลอดภัยสำหรับการเชื่อมต่อ WebSocket ต้องใช้แนวทางที่แตกต่างจาก HTTP การยืนยันตัวตนมักทำระหว่าง handshake เริ่มต้น จากนั้น session จะถูกรักษาไว้ตลอดระยะเวลาที่การเชื่อมต่อยังทำงานอยู่

typescript
import {
  WebSocketGateway,
  OnGatewayConnection,
  WsException,
} from '@nestjs/websockets';
import { Socket } from 'socket.io';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';

@WebSocketGateway()
export class AuthenticatedGateway implements OnGatewayConnection {
  constructor(
    private readonly jwtService: JwtService,
    private readonly usersService: UsersService,
  ) {}

  async handleConnection(client: Socket): Promise<void> {
    try {
      const token =
        client.handshake.auth?.token ||
        client.handshake.headers?.authorization?.split(' ')[1];

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

      const payload = await this.jwtService.verifyAsync(token);
      const user = await this.usersService.findById(payload.sub);

      if (!user) {
        throw new WsException('User not found');
      }

      // เก็บข้อมูล user ไว้ใน socket เพื่อเข้าถึงในภายหลัง
      client.data.user = user;
      client.join(`user:${user.id}`);
    } catch (error) {
      client.emit('error', { message: 'Authentication failed' });
      client.disconnect();
    }
  }
}

สำหรับการอนุญาตสิทธิ์ในระดับ message handler สามารถใช้ guard ได้เช่นเดียวกับ HTTP controller

typescript
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { WsException } from '@nestjs/websockets';
import { Socket } from 'socket.io';

@Injectable()
export class WsAuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const client: Socket = context.switchToWs().getClient();
    const user = client.data?.user;

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

    return true;
  }
}

@Injectable()
export class WsRolesGuard implements CanActivate {
  constructor(private readonly requiredRoles: string[]) {}

  canActivate(context: ExecutionContext): boolean {
    const client: Socket = context.switchToWs().getClient();
    const user = client.data?.user;

    if (!user || !this.requiredRoles.some((role) => user.roles?.includes(role))) {
      throw new WsException('Insufficient permissions');
    }

    return true;
  }
}

การใช้งาน Room และ Broadcasting

Room เป็นฟีเจอร์ที่ทรงพลังสำหรับการจัดกลุ่ม client และส่งข้อความไปยังกลุ่มที่เฉพาะเจาะจง pattern นี้มีประโยชน์มากสำหรับฟีเจอร์ต่างๆ เช่น ห้องแชท การแก้ไขร่วมกัน หรือการอัปเดตสดตามแต่ละ resource

typescript
import {
  WebSocketGateway,
  WebSocketServer,
  SubscribeMessage,
  MessageBody,
  ConnectedSocket,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { UseGuards } from '@nestjs/common';
import { WsAuthGuard } from './guards/ws-auth.guard';

interface RoomMessage {
  roomId: string;
  content: string;
  type?: 'text' | 'system';
}

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

  @SubscribeMessage('joinRoom')
  async handleJoinRoom(
    @MessageBody() data: { roomId: string },
    @ConnectedSocket() client: Socket,
  ): Promise<{ success: boolean; roomId: string }> {
    const { roomId } = data;
    const user = client.data.user;

    await client.join(roomId);

    // แจ้งสมาชิกอื่นใน room
    client.to(roomId).emit('userJoined', {
      userId: user.id,
      username: user.name,
      timestamp: Date.now(),
    });

    return { success: true, roomId };
  }

  @SubscribeMessage('leaveRoom')
  async handleLeaveRoom(
    @MessageBody() data: { roomId: string },
    @ConnectedSocket() client: Socket,
  ): Promise<{ success: boolean }> {
    const { roomId } = data;
    const user = client.data.user;

    await client.leave(roomId);

    client.to(roomId).emit('userLeft', {
      userId: user.id,
      username: user.name,
      timestamp: Date.now(),
    });

    return { success: true };
  }

  @SubscribeMessage('roomMessage')
  handleRoomMessage(
    @MessageBody() data: RoomMessage,
    @ConnectedSocket() client: Socket,
  ): void {
    const user = client.data.user;

    this.server.to(data.roomId).emit('roomMessage', {
      ...data,
      senderId: user.id,
      senderName: user.name,
      timestamp: Date.now(),
    });
  }

  // Broadcast ไปยัง client ทั้งหมดที่เชื่อมต่ออยู่
  broadcastToAll(event: string, payload: unknown): void {
    this.server.emit(event, payload);
  }

  // Broadcast ไปยัง user เฉพาะ (ทุกอุปกรณ์/แท็บ)
  broadcastToUser(userId: string, event: string, payload: unknown): void {
    this.server.to(`user:${userId}`).emit(event, payload);
  }
}

การจัดการข้อผิดพลาดและ Exception Filters

การจัดการข้อผิดพลาดที่สม่ำเสมอเป็นส่วนสำคัญในแอปพลิเคชัน WebSocket NestJS มี exception filter เฉพาะสำหรับ WebSocket ที่สามารถปรับแต่งได้ตามต้องการ

typescript
import {
  Catch,
  ArgumentsHost,
  ExceptionFilter,
  HttpException,
} from '@nestjs/common';
import { WsException } from '@nestjs/websockets';
import { Socket } from 'socket.io';

interface WsErrorResponse {
  status: 'error';
  code: string;
  message: string;
  timestamp: string;
}

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

    let errorResponse: WsErrorResponse;

    if (exception instanceof WsException) {
      const error = exception.getError();
      errorResponse = {
        status: 'error',
        code: 'WS_ERROR',
        message: typeof error === 'string' ? error : (error as { message: string }).message,
        timestamp: new Date().toISOString(),
      };
    } else if (exception instanceof HttpException) {
      errorResponse = {
        status: 'error',
        code: `HTTP_${exception.getStatus()}`,
        message: exception.message,
        timestamp: new Date().toISOString(),
      };
    } else {
      errorResponse = {
        status: 'error',
        code: 'INTERNAL_ERROR',
        message: 'An unexpected error occurred',
        timestamp: new Date().toISOString(),
      };
    }

    client.emit('exception', errorResponse);
  }
}

การทดสอบ WebSocket Gateway

การทดสอบ gateway ต้องมีการตั้งค่าพิเศษเพื่อจำลองการเชื่อมต่อ WebSocket เครื่องมือทดสอบของ NestJS สามารถรวมกับ socket.io-client สำหรับการทดสอบที่ครอบคลุม

typescript
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import { IoAdapter } from '@nestjs/platform-socket.io';
import { io, Socket as ClientSocket } from 'socket.io-client';
import { EventsGateway } from './events.gateway';

describe('EventsGateway', () => {
  let app: INestApplication;
  let clientSocket: ClientSocket;
  const PORT = 3001;

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      providers: [EventsGateway],
    }).compile();

    app = moduleFixture.createNestApplication();
    app.useWebSocketAdapter(new IoAdapter(app));
    await app.listen(PORT);
  });

  afterAll(async () => {
    if (clientSocket?.connected) {
      clientSocket.disconnect();
    }
    await app.close();
  });

  beforeEach((done) => {
    clientSocket = io(`http://localhost:${PORT}/events`, {
      transports: ['websocket'],
      autoConnect: false,
    });
    clientSocket.connect();
    clientSocket.on('connect', done);
  });

  afterEach(() => {
    if (clientSocket?.connected) {
      clientSocket.disconnect();
    }
  });

  it('should handle message event', (done) => {
    const testData = { content: 'Hello, WebSocket!' };

    clientSocket.emit('message', testData, (response: unknown) => {
      expect(response).toEqual({
        event: 'message',
        data: expect.objectContaining({ received: true }),
      });
      done();
    });
  });

  it('should broadcast to room members', (done) => {
    const secondClient = io(`http://localhost:${PORT}/events`, {
      transports: ['websocket'],
    });

    secondClient.on('connect', () => {
      // เข้าร่วม room
      clientSocket.emit('joinRoom', { roomId: 'test-room' });
      secondClient.emit('joinRoom', { roomId: 'test-room' });

      setTimeout(() => {
        secondClient.on('roomMessage', (data) => {
          expect(data.content).toBe('Test message');
          secondClient.disconnect();
          done();
        });

        clientSocket.emit('roomMessage', {
          roomId: 'test-room',
          content: 'Test message',
        });
      }, 100);
    });
  });
});

พร้อมที่จะพิชิตการสัมภาษณ์ Node.js / NestJS แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

คำถามสัมภาษณ์ทางเทคนิค

ต่อไปนี้คือคำถามที่พบบ่อยในการสัมภาษณ์ทางเทคนิคเกี่ยวกับ NestJS และ WebSockets

ความแตกต่างระหว่าง HTTP request และ WebSocket connection คืออะไร?

HTTP มีลักษณะ request-response โดยการเชื่อมต่อจะถูกปิดหลังจากส่ง response WebSocket รักษาการเชื่อมต่อแบบต่อเนื่องที่อนุญาตให้สื่อสารสองทางโดยไม่มี overhead ของ handshake ซ้ำ WebSocket เหมาะสำหรับสถานการณ์ real-time ที่ต้องการ latency ต่ำและการอัปเดตอย่างต่อเนื่อง

Gateway ใน NestJS แตกต่างจาก Controller อย่างไร?

Controller จัดการ HTTP request ด้วยรูปแบบ request-response แบบดั้งเดิม Gateway จัดการการเชื่อมต่อ WebSocket ด้วยความสามารถในการรับและส่งข้อความแบบ asynchronous ได้ทุกเมื่อขณะที่การเชื่อมต่อยังทำงานอยู่ ทั้งคู่รวมเข้ากับระบบ dependency injection ของ NestJS

เมื่อไหร่ควรใช้ Socket.IO และเมื่อไหร่ควรใช้ ws library native?

Socket.IO ถูกเลือกเมื่อต้องการฟีเจอร์ เช่น automatic reconnection, room management, namespace และ fallback ไปยัง long-polling ส่วน ws library native เหมาะกว่าสำหรับสถานการณ์ที่เน้นประสิทธิภาพสูงสุดด้วย WebSocket protocol มาตรฐานโดยไม่มี abstraction เพิ่มเติม

วิธีจัดการ authentication บน WebSocket?

การยืนยันตัวตน WebSocket ทำในช่วง handshake ผ่าน token ใน header หรือ query parameter Token ถูกตรวจสอบใน lifecycle hook handleConnection และข้อมูล user ถูกเก็บไว้ใน socket.data เพื่อเข้าถึงใน handler ต่อไป Guard สามารถใช้สำหรับการอนุญาตสิทธิ์ในแต่ละ message

กลยุทธ์สำหรับ scaling WebSocket server?

การ scale แนวนอนต้องมี shared state สำหรับการประสานงานระหว่าง instance Redis adapter อนุญาตให้ publish-subscribe ระหว่าง server เพื่อให้ message สามารถถูกส่งต่อไปยัง client ที่เชื่อมต่อกับ instance อื่น Sticky session หรือ connection draining ก็ต้องพิจารณาด้วย

วิธีจัดการ reconnection ที่ฝั่ง client?

Socket.IO มี automatic reconnection พร้อม exponential backoff แอปพลิเคชันต้องใช้ state reconciliation เมื่อ reconnect เช่น การ sync ข้อมูลใหม่หรือ replay event ที่พลาดไป Server สามารถเก็บ pending messages สำหรับ client ที่ disconnect ชั่วคราว

ความแตกต่างระหว่าง emit, broadcast และ to?

Method emit ส่งไปยัง socket ต้นทาง Method broadcast.emit ส่งไปยัง socket ทั้งหมดยกเว้นต้นทาง Method to(room).emit ส่งไปยัง socket ทั้งหมดใน room ที่กำหนด การเข้าใจความแตกต่างนี้สำคัญสำหรับการใช้งานที่ถูกต้อง

สรุป

การใช้งาน WebSocket ใน NestJS มอบ abstraction ที่ทรงพลังพร้อมกับรักษาความยืดหยุ่นที่จำเป็นสำหรับ use case ที่หลากหลาย แนวคิดของ gateway รวมกับฟีเจอร์ต่างๆ เช่น guard, interceptor และ pipe ช่วยให้นักพัฒนาสร้างแอปพลิเคชัน real-time ที่ดูแลรักษาได้และ scale ได้

การเลือก adapter, กลยุทธ์การยืนยันตัวตน และ pattern สำหรับการจัดการ room เป็นการตัดสินใจทางสถาปัตยกรรมที่ต้องปรับให้เหมาะกับความต้องการเฉพาะของแอปพลิเคชัน การทดสอบที่ครอบคลุมและการจัดการข้อผิดพลาดที่แข็งแกร่งช่วยให้แอปพลิเคชันน่าเชื่อถือใน production

ด้วยความเข้าใจอย่างลึกซึ้งเกี่ยวกับแนวคิดเหล่านี้ นักพัฒนาสามารถสร้างระบบ real-time ที่มีประสิทธิภาพและพร้อมเผชิญกับความท้าทายของการสัมภาษณ์ทางเทคนิคเกี่ยวกับหัวข้อนี้

Anthony Fillion-Maillet

เขียนโดย

Anthony Fillion-Maillet

นักพัฒนาฟูลสแตก ผู้ก่อตั้ง SharpSkill

เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่

อัปเดตเมื่อ 10 สิงหาคม 2569

แท็ก

#nestjs
#websockets
#real-time
#gateway
#nodejs

แชร์

บทความที่เกี่ยวข้อง