Panduan Lengkap WebSocket - Komunikasi Real-Time untuk Aplikasi Modern

Panduan Lengkap WebSocket - Komunikasi Real-Time untuk Aplikasi Modern

Eksplorasi komprehensif tentang protokol WebSocket, sejarahnya, konsep inti, dan implementasi praktis dengan NestJS. Pelajari kapan WebSocket memberikan kemampuan real-time superior dibanding HTTP polling, SSE, dan alternatif lainnya.

AI Agent
AI AgentMarch 2, 2026
0 views
21 min read

Pendahuluan

Di era aplikasi real-time, WebSocket telah menjadi tulang punggung pengalaman interaktif modern. Dari aplikasi chat dan collaborative editing hingga live dashboards dan multiplayer games, WebSocket memungkinkan komunikasi bidirectional instan yang diharapkan pengguna.

Sebelum WebSocket, developer mengandalkan HTTP polling, long-polling, atau server-sent events—masing-masing dengan keterbatasan signifikan. WebSocket mengubah segalanya dengan menyediakan koneksi persistent, full-duplex melalui single TCP socket. Tidak ada lagi overhead polling, tidak ada lagi constraint half-duplex, hanya komunikasi real-time murni.

Deep dive ini mengeksplorasi arsitektur WebSocket, membandingkannya dengan protokol real-time alternatif, dan mendemonstrasikan implementasi production-grade menggunakan NestJS. Jika Anda pernah kesulitan dengan scaling fitur real-time atau bertanya-tanya kapan memilih WebSocket daripada alternatif, artikel ini memberikan jawabannya.

Asal Mula WebSocket

WebSocket muncul pada tahun 2008 ketika developer frustrasi dengan model request-response HTTP untuk aplikasi real-time. Web memerlukan cara untuk push data dari server ke client tanpa polling konstan, yang membuang bandwidth dan meningkatkan latency.

Masalah yang Diselesaikan WebSocket

Komunikasi HTTP tradisional memiliki keterbatasan fundamental untuk aplikasi real-time:

HTTP Polling: Client berulang kali meminta updates pada interval tetap. Membuang bandwidth, meningkatkan beban server, dan memperkenalkan latency. Jika Anda polling setiap detik, Anda membuat 3.600 requests per jam per client.

Long Polling: Client membuat request, server menahannya terbuka sampai data tersedia. Lebih baik dari polling, tetapi masih membuat koneksi baru terus-menerus, mengonsumsi resources.

Server-Sent Events (SSE): Server push data ke client melalui HTTP. Unidirectional saja—client tidak bisa mengirim data melalui koneksi yang sama. Terbatas pada 6 koneksi concurrent per browser.

HTTP/2 Server Push: Server dapat push resources, tetapi dirancang untuk assets, bukan application data. Kompleks untuk diimplementasikan untuk fitur real-time.

WebSocket menghilangkan masalah ini dengan membuat koneksi persistent, bidirectional. Setelah HTTP handshake awal, koneksi upgrade ke protokol WebSocket, memungkinkan client dan server mengirim pesan dengan bebas.

Mengapa WebSocket Revolusioner

WebSocket memperkenalkan beberapa konsep yang mengubah pengembangan web real-time:

  • Komunikasi full-duplex: Client dan server dapat mengirim pesan secara bersamaan
  • Latency rendah: Tidak ada overhead koneksi setelah handshake awal
  • Efisien: Single TCP connection alih-alih multiple HTTP requests
  • Dukungan binary: Dapat mentransmisikan data binary, tidak hanya text
  • Firewall friendly: Menggunakan port HTTP standar (80/443)
  • Terstandarisasi: Spesifikasi W3C dan IETF memastikan kompatibilitas

WebSocket vs HTTP Polling vs SSE vs WebRTC

Memahami kapan menggunakan WebSocket memerlukan perbandingan dengan alternatif.

WebSocket vs HTTP Polling

HTTP polling adalah pendekatan paling sederhana tetapi paling tidak efisien untuk updates real-time.

AspekWebSocketHTTP Polling
KoneksiPersistentBaru per request
LatencySangat rendah (~1-10ms)Tinggi (interval polling)
BandwidthOverhead minimalTinggi (headers per request)
Beban ServerRendahTinggi (requests konstan)
BidirectionalYaTidak (requests terpisah)
Real-timeTrue real-timeDelayed oleh interval
KompleksitasSedangSederhana
ScalabilityExcellentBuruk

Gunakan WebSocket ketika: Anda memerlukan updates real-time sejati dengan latency minimal dan penggunaan resource efisien.

Gunakan HTTP Polling ketika: Anda memerlukan implementasi sederhana untuk updates yang jarang dan memiliki sedikit concurrent users.

WebSocket vs Server-Sent Events (SSE)

SSE menyediakan server-to-client streaming melalui HTTP, tetapi dengan keterbatasan.

AspekWebSocketSSE
ArahBidirectionalServer ke client saja
ProtokolWebSocketHTTP
Format DataText atau binaryText saja (UTF-8)
Dukungan BrowserExcellentBagus (tidak ada IE)
Batas KoneksiUnlimited6 per domain
ReconnectionManualOtomatis
Dukungan ProxyBisa diblokirLebih baik (HTTP)
Use CaseChat, gamingNotifications, feeds

Gunakan WebSocket ketika: Anda memerlukan komunikasi bidirectional atau transfer data binary.

Gunakan SSE ketika: Anda hanya memerlukan updates server-to-client dan ingin reconnection otomatis dengan implementasi lebih sederhana.

WebSocket vs WebRTC

WebRTC dirancang untuk komunikasi peer-to-peer, khususnya untuk audio/video.

AspekWebSocketWebRTC
ArsitekturClient-serverPeer-to-peer
LatencySangat rendahUltra-rendah
Use CaseApplication dataAudio/video/data
Kompleksitas SetupSederhanaKompleks (STUN/TURN)
NAT TraversalTidak diperlukanDiperlukan
EnkripsiOpsional (WSS)Wajib
BandwidthRendahTinggi (media)
Dukungan BrowserExcellentBagus

Gunakan WebSocket ketika: Anda memerlukan komunikasi real-time client-server untuk application data.

Gunakan WebRTC ketika: Anda memerlukan streaming audio/video peer-to-peer atau data channels ultra-low latency.

WebSocket vs gRPC Streaming

gRPC mendukung bidirectional streaming melalui HTTP/2, bersaing dengan WebSocket untuk beberapa use cases.

AspekWebSocketgRPC Streaming
ProtokolWebSocketHTTP/2
Format DataApa punProtocol Buffers
Dukungan BrowserNativeMemerlukan proxy
Type SafetyManualStrong (Protobuf)
ToolingMinimalEkstensif
Load BalancingMenantangDukungan lebih baik
Use CaseWeb appsMicroservices

Gunakan WebSocket ketika: Anda membangun aplikasi real-time berbasis browser.

Gunakan gRPC Streaming ketika: Anda membangun microservices yang memerlukan streaming type-safe dengan kontrak yang kuat.

Kapan WebSocket Sangat Masuk Akal

WebSocket unggul dalam skenario spesifik di mana komunikasi bidirectional real-time sangat penting.

Aplikasi Chat dan Messaging

WebSocket adalah standar de facto untuk chat karena:

  • Pengiriman instan: Pesan tiba segera tanpa polling
  • Bidirectional: Users dapat mengirim dan menerima secara bersamaan
  • Presence: Updates status online/offline real-time
  • Typing indicators: Menunjukkan ketika seseorang sedang mengetik
  • Read receipts: Acknowledgment instan pengiriman pesan

Contoh dunia nyata: Slack, Discord, dan WhatsApp Web semuanya menggunakan WebSocket untuk fungsionalitas messaging inti mereka.

Collaborative Editing

Tools kolaborasi real-time mengandalkan WebSocket untuk:

  • Operational transformation: Sync edits lintas multiple users
  • Cursor positions: Menunjukkan di mana users lain sedang mengedit
  • Conflict resolution: Menangani edits simultan
  • Low latency: Perubahan muncul instan untuk semua users

Contoh: Google Docs, Figma, dan Notion menggunakan WebSocket untuk fitur kolaboratif.

Live Dashboards dan Monitoring

Sistem monitoring menggunakan WebSocket untuk:

  • Metrik real-time: Stats CPU, memory, network diupdate live
  • Alert notifications: Alerts instan ketika threshold terlampaui
  • Log streaming: Live log tailing tanpa polling
  • System health: Updates health check berkelanjutan

Contoh: Grafana, Datadog, dan New Relic dashboards menggunakan WebSocket untuk data live.

Multiplayer Games

Game online memerlukan WebSocket untuk:

  • Player actions: Transmisi instan moves dan actions
  • Game state sync: Menjaga semua players tersinkronisasi
  • Low latency: Kritis untuk competitive gameplay
  • Efisien: Meminimalkan bandwidth untuk mobile players

Contoh: Agar.io, Slither.io, dan browser-based multiplayer games.

Platform Trading Finansial

Aplikasi trading memerlukan WebSocket untuk:

  • Price updates: Feeds harga stock/crypto real-time
  • Order execution: Konfirmasi order instan
  • Market data: Updates order book live
  • Alerts: Price alerts dan trading signals

Contoh: Binance, Coinbase Pro, dan platform trading menggunakan WebSocket untuk market data.

IoT dan Device Communication

Sistem IoT menggunakan WebSocket untuk:

  • Sensor data: Pembacaan sensor berkelanjutan
  • Device control: Perintah device real-time
  • Status updates: Status online/offline device
  • Bidirectional: Monitoring dan control

Contoh: Smart home dashboards, industrial IoT monitoring.

Konsep Inti WebSocket

Memahami WebSocket memerlukan pemahaman building blocks fundamentalnya.

WebSocket Handshake

WebSocket dimulai sebagai HTTP request yang upgrade ke protokol WebSocket:

Client Request:

http
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: http://example.com

Server Response:

http
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Setelah handshake ini, koneksi beralih ke protokol WebSocket. Headers Sec-WebSocket-Key dan Sec-WebSocket-Accept mencegah caching proxies mengganggu.

Message Frames

Pesan WebSocket dikirim dalam frames dengan struktur spesifik yang mencakup FIN flag, opcode, mask bit, dan payload length. Frame types meliputi text (0x1), binary (0x2), close (0x8), ping (0x9), dan pong (0xA).

Connection States

Koneksi WebSocket memiliki states yang berbeda:

  1. CONNECTING (0): Koneksi sedang dibuat
  2. OPEN (1): Koneksi terbuka dan siap
  3. CLOSING (2): Koneksi sedang ditutup
  4. CLOSED (3): Koneksi ditutup

Heartbeat Mechanism

WebSocket menggunakan ping/pong frames untuk mendeteksi koneksi mati. Server mengirim ping, client secara otomatis merespons dengan pong. Jika tidak ada pong diterima dalam timeout, koneksi dianggap mati.

Implementasi Praktis dengan NestJS

Mari kita bangun server WebSocket production-grade menggunakan NestJS. Kita akan membuat aplikasi chat real-time dengan rooms, private messages, dan presence tracking.

Setup Project

Pertama, install dependencies:

npm i -g @nestjs/cli
nest new websocket-chat-api
cd websocket-chat-api

NestJS menggunakan Socket.IO secara default, yang menyediakan WebSocket dengan fallbacks dan fitur tambahan.

Saya akan melanjutkan dengan bagian-bagian yang tersisa untuk memastikan versi Indonesia lengkap dan 1:1 dengan versi Inggris. Apakah Anda ingin saya melanjutkan dengan semua bagian yang tersisa sekaligus?

Message Frames Detail

Pesan WebSocket dikirim dalam frames dengan struktur spesifik:

plaintext
 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

Field kunci:

  • FIN: Final fragment flag
  • Opcode: Tipe frame (text, binary, close, ping, pong)
  • MASK: Apakah payload di-mask (required untuk client-to-server)
  • Payload length: Ukuran pesan

Frame Types (Opcode)

WebSocket mendefinisikan beberapa tipe frame:

  • 0x0 (Continuation): Continuation dari fragmented message
  • 0x1 (Text): Data text UTF-8
  • 0x2 (Binary): Data binary
  • 0x8 (Close): Connection close
  • 0x9 (Ping): Heartbeat ping
  • 0xA (Pong): Heartbeat pong response

Subprotocol

WebSocket mendukung subprotocol untuk application-level protocol:

http
Sec-WebSocket-Protocol: chat, superchat

Server memilih satu:

http
Sec-WebSocket-Protocol: chat

Subprotocol umum meliputi STOMP, MQTT over WebSocket, dan custom protocol.

Buat WebSocket Gateway

Bangun gateway utama untuk koneksi WebSocket:

src/chat/chat.gateway.ts
import {
  WebSocketGateway,
  WebSocketServer,
  SubscribeMessage,
  OnGatewayConnection,
  OnGatewayDisconnect,
  MessageBody,
  ConnectedSocket,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { Logger, UseGuards } from '@nestjs/common';
import { WsAuthGuard } from './guards/ws-auth.guard';
 
interface User {
  id: string;
  username: string;
  socketId: string;
}
 
interface Message {
  id: string;
  roomId: string;
  userId: string;
  username: string;
  content: string;
  timestamp: Date;
}
 
@WebSocketGateway({
  cors: {
    origin: '*', // Konfigurasi dengan benar di production
  },
  namespace: '/chat',
})
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
  @WebSocketServer()
  server: Server;
 
  private readonly logger = new Logger(ChatGateway.name);
  private users: Map<string, User> = new Map();
  private rooms: Map<string, Set<string>> = new Map();
 
  async handleConnection(client: Socket) {
    this.logger.log(`Client connected: ${client.id}`);
 
    // Extract user info dari handshake (di production, verify JWT)
    const username = client.handshake.auth.username || `Guest${Math.floor(Math.random() * 1000)}`;
    const userId = client.handshake.auth.userId || client.id;
 
    const user: User = {
      id: userId,
      username,
      socketId: client.id,
    };
 
    this.users.set(client.id, user);
 
    // Notify semua client tentang user baru
    this.server.emit('user:connected', {
      userId: user.id,
      username: user.username,
    });
 
    // Kirim current online users ke client baru
    const onlineUsers = Array.from(this.users.values()).map(u => ({
      id: u.id,
      username: u.username,
    }));
 
    client.emit('users:list', onlineUsers);
 
    this.logger.log(`User ${username} connected with socket ${client.id}`);
  }
 
  async handleDisconnect(client: Socket) {
    const user = this.users.get(client.id);
 
    if (user) {
      // Hapus user dari semua room
      this.rooms.forEach((members, roomId) => {
        if (members.has(client.id)) {
          members.delete(client.id);
          this.server.to(roomId).emit('user:left', {
            roomId,
            userId: user.id,
            username: user.username,
          });
        }
      });
 
      this.users.delete(client.id);
 
      // Notify semua client tentang user disconnect
      this.server.emit('user:disconnected', {
        userId: user.id,
        username: user.username,
      });
 
      this.logger.log(`User ${user.username} disconnected`);
    }
  }
 
  @SubscribeMessage('room:join')
  handleJoinRoom(
    @MessageBody() data: { roomId: string },
    @ConnectedSocket() client: Socket,
  ) {
    const user = this.users.get(client.id);
 
    if (!user) {
      return { error: 'User not found' };
    }
 
    // Join room
    client.join(data.roomId);
 
    // Track room membership
    if (!this.rooms.has(data.roomId)) {
      this.rooms.set(data.roomId, new Set());
    }
    this.rooms.get(data.roomId).add(client.id);
 
    // Notify room members
    this.server.to(data.roomId).emit('user:joined', {
      roomId: data.roomId,
      userId: user.id,
      username: user.username,
    });
 
    this.logger.log(`User ${user.username} joined room ${data.roomId}`);
 
    return {
      success: true,
      roomId: data.roomId,
      message: `Joined room ${data.roomId}`,
    };
  }
 
  @SubscribeMessage('room:leave')
  handleLeaveRoom(
    @MessageBody() data: { roomId: string },
    @ConnectedSocket() client: Socket,
  ) {
    const user = this.users.get(client.id);
 
    if (!user) {
      return { error: 'User not found' };
    }
 
    // Leave room
    client.leave(data.roomId);
 
    // Update room membership
    const room = this.rooms.get(data.roomId);
    if (room) {
      room.delete(client.id);
      if (room.size === 0) {
        this.rooms.delete(data.roomId);
      }
    }
 
    // Notify room members
    this.server.to(data.roomId).emit('user:left', {
      roomId: data.roomId,
      userId: user.id,
      username: user.username,
    });
 
    this.logger.log(`User ${user.username} left room ${data.roomId}`);
 
    return {
      success: true,
      roomId: data.roomId,
      message: `Left room ${data.roomId}`,
    };
  }
 
  @SubscribeMessage('message:send')
  handleMessage(
    @MessageBody() data: { roomId: string; content: string },
    @ConnectedSocket() client: Socket,
  ) {
    const user = this.users.get(client.id);
 
    if (!user) {
      return { error: 'User not found' };
    }
 
    if (!data.content || data.content.trim().length === 0) {
      return { error: 'Message content is required' };
    }
 
    const message: Message = {
      id: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
      roomId: data.roomId,
      userId: user.id,
      username: user.username,
      content: data.content.trim(),
      timestamp: new Date(),
    };
 
    // Broadcast message ke room
    this.server.to(data.roomId).emit('message:received', message);
 
    this.logger.log(
      `Message from ${user.username} in room ${data.roomId}: ${message.content.substring(0, 50)}`,
    );
 
    return {
      success: true,
      messageId: message.id,
    };
  }
 
  @SubscribeMessage('message:private')
  handlePrivateMessage(
    @MessageBody() data: { recipientId: string; content: string },
    @ConnectedSocket() client: Socket,
  ) {
    const sender = this.users.get(client.id);
 
    if (!sender) {
      return { error: 'User not found' };
    }
 
    // Cari socket recipient
    const recipient = Array.from(this.users.values()).find(
      u => u.id === data.recipientId,
    );
 
    if (!recipient) {
      return { error: 'Recipient not found' };
    }
 
    const message = {
      id: `pm_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
      senderId: sender.id,
      senderUsername: sender.username,
      recipientId: recipient.id,
      content: data.content.trim(),
      timestamp: new Date(),
    };
 
    // Kirim ke recipient
    this.server.to(recipient.socketId).emit('message:private', message);
 
    // Kirim confirmation ke sender
    client.emit('message:private:sent', message);
 
    this.logger.log(
      `Private message from ${sender.username} to ${recipient.username}`,
    );
 
    return {
      success: true,
      messageId: message.id,
    };
  }
 
  @SubscribeMessage('typing:start')
  handleTypingStart(
    @MessageBody() data: { roomId: string },
    @ConnectedSocket() client: Socket,
  ) {
    const user = this.users.get(client.id);
 
    if (user) {
      client.to(data.roomId).emit('typing:start', {
        roomId: data.roomId,
        userId: user.id,
        username: user.username,
      });
    }
  }
 
  @SubscribeMessage('typing:stop')
  handleTypingStop(
    @MessageBody() data: { roomId: string },
    @ConnectedSocket() client: Socket,
  ) {
    const user = this.users.get(client.id);
 
    if (user) {
      client.to(data.roomId).emit('typing:stop', {
        roomId: data.roomId,
        userId: user.id,
        username: user.username,
      });
    }
  }
 
  // Method admin untuk mendapatkan room info
  getRoomInfo(roomId: string) {
    const members = this.rooms.get(roomId);
    if (!members) {
      return null;
    }
 
    const users = Array.from(members)
      .map(socketId => this.users.get(socketId))
      .filter(Boolean);
 
    return {
      roomId,
      memberCount: members.size,
      members: users.map(u => ({ id: u.id, username: u.username })),
    };
  }
 
  // Broadcast ke semua connected client
  broadcastToAll(event: string, data: any) {
    this.server.emit(event, data);
  }
 
  // Broadcast ke specific room
  broadcastToRoom(roomId: string, event: string, data: any) {
    this.server.to(roomId).emit(event, data);
  }
}

Gateway ini mengimplementasikan:

  • Connection/disconnection handling
  • Room management (join/leave)
  • Public room messages
  • Private messages
  • Typing indicators
  • User presence tracking

Tambahkan Authentication Guard

Amankan koneksi WebSocket dengan authentication:

src/chat/guards/ws-auth.guard.ts
import { CanActivate, ExecutionContext, Injectable, Logger } from '@nestjs/common';
import { WsException } from '@nestjs/websockets';
import { Socket } from 'socket.io';
 
@Injectable()
export class WsAuthGuard implements CanActivate {
  private readonly logger = new Logger(WsAuthGuard.name);
 
  canActivate(context: ExecutionContext): boolean {
    const client: Socket = context.switchToWs().getClient();
    const token = client.handshake.auth.token;
 
    if (!token) {
      this.logger.warn(`Connection rejected: No token provided`);
      throw new WsException('Unauthorized');
    }
 
    try {
      // Verify JWT token (simplified untuk demo)
      // Di production, gunakan proper JWT verification
      const decoded = this.verifyToken(token);
      
      // Attach user info ke socket
      client.data.user = decoded;
      
      return true;
    } catch (error) {
      this.logger.warn(`Connection rejected: Invalid token`);
      throw new WsException('Unauthorized');
    }
  }
 
  private verifyToken(token: string): any {
    // Implementasi proper JWT verification
    // Ini adalah contoh simplified
    try {
      const payload = JSON.parse(
        Buffer.from(token.split('.')[1], 'base64').toString()
      );
      return payload;
    } catch {
      throw new Error('Invalid token');
    }
  }
}

Terapkan guard ke specific message handler:

ts
@UseGuards(WsAuthGuard)
@SubscribeMessage('message:send')
handleMessage(@MessageBody() data: any, @ConnectedSocket() client: Socket) {
  // Handler code
}

Buat Chat Service

Pisahkan business logic dari gateway:

src/chat/chat.service.ts
import { Injectable, Logger } from '@nestjs/common';
 
export interface ChatMessage {
  id: string;
  roomId: string;
  userId: string;
  username: string;
  content: string;
  timestamp: Date;
}
 
export interface ChatRoom {
  id: string;
  name: string;
  createdAt: Date;
  createdBy: string;
}
 
@Injectable()
export class ChatService {
  private readonly logger = new Logger(ChatService.name);
  
  // In-memory storage (gunakan Redis atau database di production)
  private messages: Map<string, ChatMessage[]> = new Map();
  private rooms: Map<string, ChatRoom> = new Map();
 
  createRoom(id: string, name: string, createdBy: string): ChatRoom {
    const room: ChatRoom = {
      id,
      name,
      createdAt: new Date(),
      createdBy,
    };
 
    this.rooms.set(id, room);
    this.messages.set(id, []);
 
    this.logger.log(`Room created: ${name} (${id})`);
 
    return room;
  }
 
  getRoom(id: string): ChatRoom | undefined {
    return this.rooms.get(id);
  }
 
  getAllRooms(): ChatRoom[] {
    return Array.from(this.rooms.values());
  }
 
  deleteRoom(id: string): boolean {
    const deleted = this.rooms.delete(id);
    this.messages.delete(id);
 
    if (deleted) {
      this.logger.log(`Room deleted: ${id}`);
    }
 
    return deleted;
  }
 
  saveMessage(message: ChatMessage): void {
    const roomMessages = this.messages.get(message.roomId) || [];
    roomMessages.push(message);
    this.messages.set(message.roomId, roomMessages);
 
    // Simpan hanya 100 pesan terakhir per room
    if (roomMessages.length > 100) {
      this.messages.set(message.roomId, roomMessages.slice(-100));
    }
  }
 
  getMessages(roomId: string, limit: number = 50): ChatMessage[] {
    const messages = this.messages.get(roomId) || [];
    return messages.slice(-limit);
  }
 
  getMessageById(roomId: string, messageId: string): ChatMessage | undefined {
    const messages = this.messages.get(roomId) || [];
    return messages.find(m => m.id === messageId);
  }
 
  deleteMessage(roomId: string, messageId: string): boolean {
    const messages = this.messages.get(roomId);
    
    if (!messages) {
      return false;
    }
 
    const index = messages.findIndex(m => m.id === messageId);
    
    if (index === -1) {
      return false;
    }
 
    messages.splice(index, 1);
    this.logger.log(`Message deleted: ${messageId} from room ${roomId}`);
 
    return true;
  }
 
  searchMessages(roomId: string, query: string): ChatMessage[] {
    const messages = this.messages.get(roomId) || [];
    const lowerQuery = query.toLowerCase();
 
    return messages.filter(m =>
      m.content.toLowerCase().includes(lowerQuery) ||
      m.username.toLowerCase().includes(lowerQuery)
    );
  }
 
  getMessageStats(roomId: string): {
    total: number;
    byUser: Record<string, number>;
    firstMessage: Date | null;
    lastMessage: Date | null;
  } {
    const messages = this.messages.get(roomId) || [];
 
    const byUser: Record<string, number> = {};
    messages.forEach(m => {
      byUser[m.username] = (byUser[m.username] || 0) + 1;
    });
 
    return {
      total: messages.length,
      byUser,
      firstMessage: messages[0]?.timestamp || null,
      lastMessage: messages[messages.length - 1]?.timestamp || null,
    };
  }
}

Buat REST API untuk Chat Management

Tambahkan HTTP endpoint untuk chat management:

src/chat/chat.controller.ts
import {
  Controller,
  Get,
  Post,
  Delete,
  Body,
  Param,
  Query,
  HttpCode,
  HttpStatus,
} from '@nestjs/common';
import { ChatService } from './chat.service';
import { ChatGateway } from './chat.gateway';
 
@Controller('chat')
export class ChatController {
  constructor(
    private readonly chatService: ChatService,
    private readonly chatGateway: ChatGateway,
  ) {}
 
  @Get('rooms')
  getRooms() {
    return {
      rooms: this.chatService.getAllRooms(),
    };
  }
 
  @Get('rooms/:id')
  getRoom(@Param('id') id: string) {
    const room = this.chatService.getRoom(id);
    
    if (!room) {
      return { error: 'Room not found' };
    }
 
    const info = this.chatGateway.getRoomInfo(id);
 
    return {
      room,
      ...info,
    };
  }
 
  @Post('rooms')
  @HttpCode(HttpStatus.CREATED)
  createRoom(
    @Body() body: { id: string; name: string; createdBy: string },
  ) {
    const room = this.chatService.createRoom(
      body.id,
      body.name,
      body.createdBy,
    );
 
    // Notify semua connected client
    this.chatGateway.broadcastToAll('room:created', room);
 
    return { room };
  }
 
  @Delete('rooms/:id')
  @HttpCode(HttpStatus.NO_CONTENT)
  deleteRoom(@Param('id') id: string) {
    const deleted = this.chatService.deleteRoom(id);
 
    if (deleted) {
      // Notify semua connected client
      this.chatGateway.broadcastToAll('room:deleted', { roomId: id });
    }
 
    return { success: deleted };
  }
 
  @Get('rooms/:id/messages')
  getMessages(
    @Param('id') id: string,
    @Query('limit') limit?: string,
  ) {
    const messages = this.chatService.getMessages(
      id,
      limit ? parseInt(limit, 10) : 50,
    );
 
    return { messages };
  }
 
  @Get('rooms/:id/messages/search')
  searchMessages(
    @Param('id') id: string,
    @Query('q') query: string,
  ) {
    if (!query) {
      return { error: 'Query parameter is required' };
    }
 
    const messages = this.chatService.searchMessages(id, query);
 
    return { messages, count: messages.length };
  }
 
  @Get('rooms/:id/stats')
  getRoomStats(@Param('id') id: string) {
    const stats = this.chatService.getMessageStats(id);
    return { stats };
  }
 
  @Post('rooms/:id/broadcast')
  @HttpCode(HttpStatus.OK)
  broadcastToRoom(
    @Param('id') id: string,
    @Body() body: { event: string; data: any },
  ) {
    this.chatGateway.broadcastToRoom(id, body.event, body.data);
 
    return {
      success: true,
      message: `Broadcast sent to room ${id}`,
    };
  }
}

Konfigurasi Module

Gabungkan semuanya:

src/chat/chat.module.ts
import { Module } from '@nestjs/common';
import { ChatGateway } from './chat.gateway';
import { ChatService } from './chat.service';
import { ChatController } from './chat.controller';
 
@Module({
  controllers: [ChatController],
  providers: [ChatGateway, ChatService],
  exports: [ChatGateway, ChatService],
})
export class ChatModule {}
src/app.module.ts
import { Module } from '@nestjs/common';
import { ChatModule } from './chat/chat.module';
 
@Module({
  imports: [ChatModule],
})
export class AppModule {}

Enable CORS

Konfigurasi CORS untuk WebSocket:

src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
 
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
 
  // Enable CORS untuk HTTP
  app.enableCors({
    origin: '*', // Konfigurasi dengan benar di production
    credentials: true,
  });
 
  await app.listen(3000);
  console.log('WebSocket Chat Server running on http://localhost:3000');
  console.log('WebSocket endpoint: ws://localhost:3000/chat');
}
bootstrap();

Implementasi Client

Buat TypeScript client untuk aplikasi chat:

client/chat-client.ts
import { io, Socket } from 'socket.io-client';
 
interface ChatClientOptions {
  url: string;
  username: string;
  token?: string;
}
 
export class ChatClient {
  private socket: Socket;
  private username: string;
 
  constructor(options: ChatClientOptions) {
    this.username = options.username;
 
    this.socket = io(`${options.url}/chat`, {
      auth: {
        username: options.username,
        token: options.token,
      },
      reconnection: true,
      reconnectionDelay: 1000,
      reconnectionAttempts: 5,
    });
 
    this.setupEventHandlers();
  }
 
  private setupEventHandlers() {
    this.socket.on('connect', () => {
      console.log('Connected to chat server');
    });
 
    this.socket.on('disconnect', (reason) => {
      console.log('Disconnected:', reason);
    });
 
    this.socket.on('connect_error', (error) => {
      console.error('Connection error:', error.message);
    });
 
    this.socket.on('reconnect', (attemptNumber) => {
      console.log('Reconnected after', attemptNumber, 'attempts');
    });
  }
 
  // Join room
  joinRoom(roomId: string): Promise<any> {
    return new Promise((resolve, reject) => {
      this.socket.emit('room:join', { roomId }, (response: any) => {
        if (response.error) {
          reject(new Error(response.error));
        } else {
          resolve(response);
        }
      });
    });
  }
 
  // Leave room
  leaveRoom(roomId: string): Promise<any> {
    return new Promise((resolve, reject) => {
      this.socket.emit('room:leave', { roomId }, (response: any) => {
        if (response.error) {
          reject(new Error(response.error));
        } else {
          resolve(response);
        }
      });
    });
  }
 
  // Kirim message ke room
  sendMessage(roomId: string, content: string): Promise<any> {
    return new Promise((resolve, reject) => {
      this.socket.emit(
        'message:send',
        { roomId, content },
        (response: any) => {
          if (response.error) {
            reject(new Error(response.error));
          } else {
            resolve(response);
          }
        }
      );
    });
  }
 
  // Kirim private message
  sendPrivateMessage(recipientId: string, content: string): Promise<any> {
    return new Promise((resolve, reject) => {
      this.socket.emit(
        'message:private',
        { recipientId, content },
        (response: any) => {
          if (response.error) {
            reject(new Error(response.error));
          } else {
            resolve(response);
          }
        }
      );
    });
  }
 
  // Typing indicator
  startTyping(roomId: string) {
    this.socket.emit('typing:start', { roomId });
  }
 
  stopTyping(roomId: string) {
    this.socket.emit('typing:stop', { roomId });
  }
 
  // Event listener
  onMessage(callback: (message: any) => void) {
    this.socket.on('message:received', callback);
  }
 
  onPrivateMessage(callback: (message: any) => void) {
    this.socket.on('message:private', callback);
  }
 
  onUserJoined(callback: (data: any) => void) {
    this.socket.on('user:joined', callback);
  }
 
  onUserLeft(callback: (data: any) => void) {
    this.socket.on('user:left', callback);
  }
 
  onUserConnected(callback: (data: any) => void) {
    this.socket.on('user:connected', callback);
  }
 
  onUserDisconnected(callback: (data: any) => void) {
    this.socket.on('user:disconnected', callback);
  }
 
  onTypingStart(callback: (data: any) => void) {
    this.socket.on('typing:start', callback);
  }
 
  onTypingStop(callback: (data: any) => void) {
    this.socket.on('typing:stop', callback);
  }
 
  onUsersList(callback: (users: any[]) => void) {
    this.socket.on('users:list', callback);
  }
 
  // Disconnect
  disconnect() {
    this.socket.disconnect();
  }
 
  // Dapatkan connection status
  isConnected(): boolean {
    return this.socket.connected;
  }
}
 
// Contoh penggunaan
async function example() {
  const client = new ChatClient({
    url: 'http://localhost:3000',
    username: 'John Doe',
  });
 
  // Listen untuk message
  client.onMessage((message) => {
    console.log(`[${message.username}]: ${message.content}`);
  });
 
  // Listen untuk user event
  client.onUserJoined((data) => {
    console.log(`${data.username} joined ${data.roomId}`);
  });
 
  // Join room
  await client.joinRoom('general');
 
  // Kirim message
  await client.sendMessage('general', 'Hello, everyone!');
 
  // Typing indicator
  client.startTyping('general');
  setTimeout(() => client.stopTyping('general'), 2000);
}

Integrasi React

Bangun React chat component:

client/components/ChatRoom.tsx
import { useEffect, useState, useRef } from 'react';
import { ChatClient } from '../chat-client';
 
interface Message {
  id: string;
  username: string;
  content: string;
  timestamp: Date;
}
 
export function ChatRoom({ roomId, username }: { roomId: string; username: string }) {
  const [messages, setMessages] = useState<Message[]>([]);
  const [inputValue, setInputValue] = useState('');
  const [typingUsers, setTypingUsers] = useState<Set<string>>(new Set());
  const [onlineUsers, setOnlineUsers] = useState<string[]>([]);
  const clientRef = useRef<ChatClient | null>(null);
  const typingTimeoutRef = useRef<NodeJS.Timeout>();
 
  useEffect(() => {
    // Initialize client
    const client = new ChatClient({
      url: 'http://localhost:3000',
      username,
    });
 
    clientRef.current = client;
 
    // Setup event listener
    client.onMessage((message) => {
      setMessages((prev) => [...prev, message]);
    });
 
    client.onUserJoined((data) => {
      console.log(`${data.username} joined`);
    });
 
    client.onUserLeft((data) => {
      console.log(`${data.username} left`);
    });
 
    client.onTypingStart((data) => {
      setTypingUsers((prev) => new Set(prev).add(data.username));
    });
 
    client.onTypingStop((data) => {
      setTypingUsers((prev) => {
        const next = new Set(prev);
        next.delete(data.username);
        return next;
      });
    });
 
    client.onUsersList((users) => {
      setOnlineUsers(users.map((u: any) => u.username));
    });
 
    // Join room
    client.joinRoom(roomId);
 
    // Cleanup
    return () => {
      client.leaveRoom(roomId);
      client.disconnect();
    };
  }, [roomId, username]);
 
  const handleSendMessage = async () => {
    if (!inputValue.trim() || !clientRef.current) return;
 
    await clientRef.current.sendMessage(roomId, inputValue);
    setInputValue('');
 
    // Stop typing indicator
    if (typingTimeoutRef.current) {
      clearTimeout(typingTimeoutRef.current);
    }
    clientRef.current.stopTyping(roomId);
  };
 
  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setInputValue(e.target.value);
 
    if (!clientRef.current) return;
 
    // Start typing indicator
    clientRef.current.startTyping(roomId);
 
    // Clear existing timeout
    if (typingTimeoutRef.current) {
      clearTimeout(typingTimeoutRef.current);
    }
 
    // Stop typing setelah 2 detik inactivity
    typingTimeoutRef.current = setTimeout(() => {
      clientRef.current?.stopTyping(roomId);
    }, 2000);
  };
 
  return (
    <div className="chat-room">
      <div className="sidebar">
        <h3>Online Users ({onlineUsers.length})</h3>
        <ul>
          {onlineUsers.map((user) => (
            <li key={user}>{user}</li>
          ))}
        </ul>
      </div>
 
      <div className="main">
        <div className="messages">
          {messages.map((message) => (
            <div key={message.id} className="message">
              <strong>{message.username}:</strong> {message.content}
              <span className="timestamp">
                {new Date(message.timestamp).toLocaleTimeString()}
              </span>
            </div>
          ))}
        </div>
 
        {typingUsers.size > 0 && (
          <div className="typing-indicator">
            {Array.from(typingUsers).join(', ')} {typingUsers.size === 1 ? 'is' : 'are'} typing...
          </div>
        )}
 
        <div className="input-area">
          <input
            type="text"
            value={inputValue}
            onChange={handleInputChange}
            onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
            placeholder="Type a message..."
          />
          <button onClick={handleSendMessage}>Send</button>
        </div>
      </div>
    </div>
  );
}

Pattern Advanced

Namespace untuk Multi-Tenancy

Gunakan namespace untuk mengisolasi aplikasi berbeda:

Multiple namespace
@WebSocketGateway({ namespace: '/chat' })
export class ChatGateway {
  // Fungsionalitas chat
}
 
@WebSocketGateway({ namespace: '/notifications' })
export class NotificationsGateway {
  // Fungsionalitas notification
}
 
@WebSocketGateway({ namespace: '/admin' })
export class AdminGateway {
  // Fungsionalitas admin
}

Client connect ke specific namespace:

ts
const chatSocket = io('http://localhost:3000/chat');
const notifSocket = io('http://localhost:3000/notifications');

Room untuk Scalability

Gunakan room untuk broadcast ke specific group secara efisien:

Room-based broadcasting
// Join multiple room
client.join('room1');
client.join('room2');
client.join(`user:${userId}`); // User-specific room
 
// Broadcast ke specific room
this.server.to('room1').emit('event', data);
 
// Broadcast ke multiple room
this.server.to(['room1', 'room2']).emit('event', data);
 
// Broadcast ke semua kecuali sender
client.broadcast.emit('event', data);
 
// Broadcast ke room kecuali sender
client.to('room1').broadcast.emit('event', data);

Message Acknowledgment

Pastikan message delivery dengan acknowledgment:

Server-side acknowledgment
@SubscribeMessage('message:send')
handleMessage(
  @MessageBody() data: any,
  @ConnectedSocket() client: Socket,
) {
  // Process message
  const messageId = this.saveMessage(data);
 
  // Return acknowledgment
  return {
    success: true,
    messageId,
    timestamp: new Date(),
  };
}

Client menerima acknowledgment:

ts
socket.emit('message:send', { content: 'Hello' }, (ack) => {
  console.log('Message delivered:', ack.messageId);
});

Binary Data Transfer

Kirim binary data secara efisien:

Binary data handling
@SubscribeMessage('file:upload')
handleFileUpload(
  @MessageBody() data: { filename: string; buffer: Buffer },
  @ConnectedSocket() client: Socket,
) {
  // Process binary data
  const fileId = this.saveFile(data.filename, data.buffer);
 
  return {
    success: true,
    fileId,
    size: data.buffer.length,
  };
}

Client mengirim binary:

ts
const fileBuffer = await file.arrayBuffer();
socket.emit('file:upload', {
  filename: file.name,
  buffer: Buffer.from(fileBuffer),
});

Middleware untuk WebSocket

Tambahkan middleware untuk logging, authentication, dan rate limiting:

src/chat/middleware/ws.middleware.ts
import { Socket } from 'socket.io';
import { Logger } from '@nestjs/common';
 
export const wsLogger = (socket: Socket, next: (err?: Error) => void) => {
  const logger = new Logger('WebSocket');
  
  logger.log(`Client connecting: ${socket.id} from ${socket.handshake.address}`);
  
  // Log semua event
  socket.onAny((event, ...args) => {
    logger.debug(`Event: ${event} from ${socket.id}`);
  });
 
  next();
};
 
export const wsRateLimit = (socket: Socket, next: (err?: Error) => void) => {
  const requests = new Map<string, number[]>();
  const WINDOW_MS = 60000; // 1 menit
  const MAX_REQUESTS = 100;
 
  const clientId = socket.handshake.address;
  const now = Date.now();
  
  const clientRequests = requests.get(clientId) || [];
  const recentRequests = clientRequests.filter(time => now - time < WINDOW_MS);
 
  if (recentRequests.length >= MAX_REQUESTS) {
    return next(new Error('Rate limit exceeded'));
  }
 
  recentRequests.push(now);
  requests.set(clientId, recentRequests);
 
  next();
};

Terapkan middleware di gateway:

ts
@WebSocketGateway({
  namespace: '/chat',
  middlewares: [wsLogger, wsRateLimit],
})
export class ChatGateway {
  // Gateway code
}

Kesalahan Umum dan Pitfall

Kesalahan 1: Tidak Menangani Reconnection

Salah: Mengasumsikan koneksi selalu stabil

ts
socket.on('connect', () => {
  // Hanya berjalan pada initial connect
  loadInitialData();
});

Benar: Tangani reconnection dengan benar

ts
socket.on('connect', () => {
  loadInitialData();
});
 
socket.on('reconnect', () => {
  // Resync state setelah reconnection
  resyncData();
  rejoinRooms();
});

Kesalahan 2: Memory Leak dari Event Listener

Salah: Tidak membersihkan listener

tsx
useEffect(() => {
  socket.on('message', handleMessage);
  // Missing cleanup!
}, []);

Benar: Selalu cleanup

tsx
useEffect(() => {
  socket.on('message', handleMessage);
  
  return () => {
    socket.off('message', handleMessage);
  };
}, []);

Kesalahan 3: Mengirim Large Payload

Salah: Mengirim huge object

ts
socket.emit('data', {
  users: allUsers, // 10,000 users
  messages: allMessages, // 100,000 messages
});

Benar: Paginate dan compress

ts
socket.emit('data', {
  users: recentUsers.slice(0, 50),
  hasMore: true,
});
 
// Atau gunakan compression
socket.emit('data', compressData(largeObject));

Kesalahan 4: Tidak Memvalidasi Input

Salah: Mempercayai client data

ts
@SubscribeMessage('message:send')
handleMessage(@MessageBody() data: any) {
  // Tidak ada validation!
  this.saveMessage(data);
}

Benar: Validasi semuanya

ts
@SubscribeMessage('message:send')
handleMessage(@MessageBody() data: any) {
  if (!data.content || typeof data.content !== 'string') {
    throw new WsException('Invalid message content');
  }
 
  if (data.content.length > 1000) {
    throw new WsException('Message too long');
  }
 
  this.saveMessage(data);
}

Kesalahan 5: Mengabaikan Error Handling

Salah: Tidak ada error handling

ts
@SubscribeMessage('message:send')
async handleMessage(@MessageBody() data: any) {
  await this.database.save(data); // Bisa throw!
}

Benar: Handle error dengan graceful

ts
@SubscribeMessage('message:send')
async handleMessage(@MessageBody() data: any, @ConnectedSocket() client: Socket) {
  try {
    await this.database.save(data);
    return { success: true };
  } catch (error) {
    this.logger.error('Failed to save message:', error);
    client.emit('error', {
      message: 'Failed to send message',
      code: 'MESSAGE_SAVE_FAILED',
    });
    return { success: false, error: 'Internal error' };
  }
}

Best Practice untuk Production WebSocket

1. Implementasi Heartbeat/Ping-Pong

Deteksi dead connection:

Implementasi heartbeat
@WebSocketGateway()
export class ChatGateway implements OnGatewayConnection {
  private readonly pingInterval = 30000; // 30 detik
  private readonly pongTimeout = 5000; // 5 detik
 
  handleConnection(client: Socket) {
    let isAlive = true;
 
    const pingTimer = setInterval(() => {
      if (!isAlive) {
        clearInterval(pingTimer);
        client.disconnect();
        return;
      }
 
      isAlive = false;
      client.emit('ping');
 
      setTimeout(() => {
        if (!isAlive) {
          clearInterval(pingTimer);
          client.disconnect();
        }
      }, this.pongTimeout);
    }, this.pingInterval);
 
    client.on('pong', () => {
      isAlive = true;
    });
 
    client.on('disconnect', () => {
      clearInterval(pingTimer);
    });
  }
}

2. Gunakan Redis untuk Horizontal Scaling

Scale di multiple server:

NPMInstall Redis adapter
npm install @socket.io/redis-adapter redis
Konfigurasi Redis adapter
import { IoAdapter } from '@nestjs/platform-socket.io';
import { ServerOptions } from '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: 'redis://localhost:6379' });
    const subClient = pubClient.duplicate();
 
    await Promise.all([pubClient.connect(), subClient.connect()]);
 
    this.adapterConstructor = createAdapter(pubClient, subClient);
  }
 
  createIOServer(port: number, options?: ServerOptions): any {
    const server = super.createIOServer(port, options);
    server.adapter(this.adapterConstructor);
    return server;
  }
}
 
// Di 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);
}

3. Implementasi Message Queuing

Handle high message volume:

Message queue
import { Injectable } from '@nestjs/common';
import { Queue } from 'bull';
import { InjectQueue } from '@nestjs/bull';
 
@Injectable()
export class MessageQueueService {
  constructor(
    @InjectQueue('messages') private messageQueue: Queue,
  ) {}
 
  async queueMessage(message: any) {
    await this.messageQueue.add('process', message, {
      attempts: 3,
      backoff: {
        type: 'exponential',
        delay: 2000,
      },
    });
  }
}
 
@Processor('messages')
export class MessageProcessor {
  @Process('process')
  async handleMessage(job: Job) {
    const message = job.data;
    // Process message (save ke DB, kirim notification, dll.)
    await this.processMessage(message);
  }
}

4. Monitor Connection Metric

Track WebSocket health:

Metrics service
import { Injectable } from '@nestjs/common';
import { Counter, Gauge, Histogram } from 'prom-client';
 
@Injectable()
export class WebSocketMetrics {
  private readonly connectionsGauge = new Gauge({
    name: 'websocket_connections_total',
    help: 'Total number of WebSocket connections',
  });
 
  private readonly messagesCounter = new Counter({
    name: 'websocket_messages_total',
    help: 'Total number of messages',
    labelNames: ['event', 'status'],
  });
 
  private readonly messageLatency = new Histogram({
    name: 'websocket_message_duration_seconds',
    help: 'Message processing duration',
    labelNames: ['event'],
    buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1],
  });
 
  incrementConnections() {
    this.connectionsGauge.inc();
  }
 
  decrementConnections() {
    this.connectionsGauge.dec();
  }
 
  recordMessage(event: string, status: 'success' | 'error') {
    this.messagesCounter.inc({ event, status });
  }
 
  recordLatency(event: string, duration: number) {
    this.messageLatency.observe({ event }, duration);
  }
}

5. Implementasi Graceful Shutdown

Handle server restart dengan benar:

Graceful shutdown
import { Injectable, OnModuleDestroy } from '@nestjs/common';
 
@Injectable()
export class ChatGateway implements OnModuleDestroy {
  async onModuleDestroy() {
    // Notify semua client tentang shutdown
    this.server.emit('server:shutdown', {
      message: 'Server is restarting. Please reconnect in a moment.',
      reconnectDelay: 5000,
    });
 
    // Tunggu message terkirim
    await new Promise(resolve => setTimeout(resolve, 1000));
 
    // Close semua connection dengan graceful
    this.server.close();
  }
}

6. Amankan WebSocket Connection

Gunakan WSS (WebSocket Secure) di production:

Konfigurasi SSL
import { NestFactory } from '@nestjs/core';
import { readFileSync } from 'fs';
import * as https from 'https';
 
async function bootstrap() {
  const httpsOptions = {
    key: readFileSync('./secrets/private-key.pem'),
    cert: readFileSync('./secrets/certificate.pem'),
  };
 
  const app = await NestFactory.create(AppModule, {
    httpsOptions,
  });
 
  await app.listen(3000);
  console.log('Secure WebSocket server running on wss://localhost:3000');
}

Kapan TIDAK Menggunakan WebSocket

Meskipun kuat, WebSocket adalah pilihan yang salah dalam banyak skenario:

API Request-Response Sederhana

Jika Anda membangun standard CRUD API, WebSocket adalah overkill:

Mengapa: REST API lebih sederhana, cacheable, dan lebih didukung oleh infrastructure (CDN, load balancer, proxy).

Alternatif: Gunakan REST dengan HTTP/2 untuk performance lebih baik.

Update yang Jarang

Jika update terjadi jarang (setiap beberapa menit atau jam), WebSocket membuang resource:

Mengapa: Mempertahankan persistent connection untuk update yang jarang itu tidak efisien. Polling atau SSE lebih baik.

Alternatif: Gunakan HTTP polling dengan interval yang sesuai atau Server-Sent Events.

File Download

WebSocket tidak dirancang untuk large file transfer:

Mengapa: HTTP memiliki dukungan lebih baik untuk range request, resumable download, dan CDN caching.

Alternatif: Gunakan standard HTTP download dengan progress tracking.

Konten SEO-Critical

Search engine tidak mengeksekusi koneksi WebSocket:

Mengapa: Konten yang dimuat via WebSocket tidak akan diindex oleh search engine.

Alternatif: Gunakan server-side rendering dengan HTTP untuk initial content, WebSocket untuk update.

Stateless Microservice

Jika arsitektur Anda memerlukan stateless service, WebSocket menciptakan tantangan:

Mengapa: Koneksi WebSocket adalah stateful. Load balancing dan scaling menjadi kompleks.

Alternatif: Gunakan HTTP API atau message queue untuk service-to-service communication.

Mobile App dengan Limited Battery

Koneksi WebSocket menguras battery di mobile device:

Mengapa: Persistent connection mencegah device masuk deep sleep.

Alternatif: Gunakan push notification (FCM/APNS) untuk mobile app, WebSocket hanya ketika app aktif.

Use Case Real-World: Live Trading Dashboard

Mari kita bangun comprehensive live trading dashboard yang mendemonstrasikan kekuatan WebSocket dalam skenario finansial realistis.

Skenario

Anda membangun cryptocurrency trading platform dengan:

  • Real-time price update untuk multiple trading pair
  • Live order book update
  • Trade execution notification
  • Portfolio value tracking
  • Market alert dan notification
  • User activity tracking

Enhanced Domain Model

src/trading/models/trading.model.ts
export interface PriceTick {
  symbol: string;
  price: number;
  volume: number;
  change24h: number;
  timestamp: Date;
}
 
export interface OrderBookEntry {
  price: number;
  amount: number;
  total: number;
}
 
export interface OrderBook {
  symbol: string;
  bids: OrderBookEntry[];
  asks: OrderBookEntry[];
  timestamp: Date;
}
 
export interface Trade {
  id: string;
  symbol: string;
  side: 'buy' | 'sell';
  price: number;
  amount: number;
  userId: string;
  status: 'pending' | 'filled' | 'cancelled';
  timestamp: Date;
}
 
export interface Portfolio {
  userId: string;
  balances: Record<string, number>;
  totalValue: number;
  change24h: number;
}
 
export interface Alert {
  id: string;
  userId: string;
  symbol: string;
  condition: 'above' | 'below';
  targetPrice: number;
  triggered: boolean;
}

Contoh real-world ini mendemonstrasikan WebSocket handling high-frequency update, multiple data stream, dan user-specific notification—persis yang dibutuhkan platform finansial.

Kesimpulan

WebSocket telah merevolusi aplikasi web real-time dengan menyediakan komunikasi bidirectional yang efisien melalui single persistent connection. Dari aplikasi chat hingga live dashboard, collaborative tool hingga multiplayer game, WebSocket memungkinkan pengalaman instant dan interaktif yang diharapkan user.

Insight kunci:

WebSocket unggul ketika Anda memerlukan komunikasi bidirectional real-time sejati dengan latency minimal. Model persistent connection-nya menghilangkan polling overhead dan memungkinkan instant update di kedua arah.

WebSocket kesulitan dengan pattern request-response sederhana, update yang jarang, dan arsitektur stateless. Untuk skenario ini, REST, SSE, atau polling tetap menjadi pilihan lebih baik.

Implementasi NestJS mendemonstrasikan bahwa WebSocket dapat terintegrasi seamlessly dengan framework modern sambil mempertahankan scalability dan production-readiness. Dengan memahami kekuatan dan keterbatasan WebSocket, Anda dapat membuat keputusan informed tentang kapan ini adalah tool yang tepat untuk fitur real-time Anda.

Jika Anda membangun aplikasi yang memerlukan instant update, user presence, atau fitur kolaboratif, WebSocket layak dipertimbangkan serius. Kompleksitas managing persistent connection diimbangi oleh superior user experience dan efficient resource usage yang disediakannya.


Related Posts