NestJS dan WebSockets di 2026: Real-Time, Gateway, dan Pertanyaan Wawancara

Panduan lengkap untuk membangun aplikasi real-time dengan NestJS dan WebSockets, termasuk implementasi gateway, best practices, dan persiapan wawancara teknis.

NestJS WebSockets Real-Time Gateway

Komunikasi real-time telah menjadi kebutuhan fundamental dalam pengembangan aplikasi modern. Dari aplikasi chat, notifikasi langsung, hingga dashboard monitoring, WebSockets menawarkan solusi efisien untuk komunikasi dua arah antara client dan server. NestJS, sebagai framework Node.js yang powerful, menyediakan abstraksi yang elegan untuk bekerja dengan WebSockets melalui konsep Gateway.

Artikel ini membahas implementasi WebSockets di NestJS secara mendalam, mulai dari konsep dasar hingga pattern lanjutan yang digunakan di production. Pembahasan juga mencakup pertanyaan-pertanyaan wawancara teknis yang sering muncul terkait topik ini.

NestJS 11 memperkenalkan peningkatan performa signifikan pada WebSocket adapter, termasuk dukungan native untuk binary messages dan compression. Pastikan menggunakan versi terbaru untuk mendapatkan fitur-fitur optimal.

Memahami WebSocket Gateway di NestJS

Gateway merupakan class yang didekorasi dengan @WebSocketGateway() dan berfungsi sebagai entry point untuk koneksi WebSocket. Berbeda dengan controller HTTP biasa, gateway memungkinkan komunikasi bidirectional yang persisten antara client dan server.

Konsep gateway di NestJS terinspirasi dari pattern yang sama di framework lain, namun dengan integrasi penuh ke dalam ekosistem dependency injection NestJS. Hal ini memungkinkan penggunaan service, guard, interceptor, dan pipe yang sama seperti pada 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() } };
  }
}

Kode di atas menunjukkan implementasi dasar gateway dengan lifecycle hooks. Method afterInit, handleConnection, dan handleDisconnect memberikan kontrol penuh atas siklus hidup koneksi WebSocket.

Konfigurasi Adapter: Socket.IO vs ws

NestJS mendukung dua adapter utama untuk WebSocket: Socket.IO dan ws. Pemilihan adapter bergantung pada kebutuhan spesifik aplikasi.

Socket.IO menyediakan fitur tambahan seperti automatic reconnection, room management, dan fallback ke polling. Adapter ws lebih ringan dan cocok untuk skenario yang membutuhkan performa maksimal dengan overhead minimal.

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);

  // Menggunakan Socket.IO adapter
  app.useWebSocketAdapter(new IoAdapter(app));

  // Atau menggunakan ws adapter
  // app.useWebSocketAdapter(new WsAdapter(app));

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

Untuk aplikasi yang membutuhkan skalabilitas horizontal, Redis adapter menjadi pilihan yang tepat. Adapter ini memungkinkan multiple instance server untuk berbagi state koneksi.

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;
  }
}

// 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);
}

Autentikasi dan Autorisasi WebSocket

Keamanan pada koneksi WebSocket memerlukan pendekatan berbeda dibandingkan HTTP. Autentikasi biasanya dilakukan saat handshake awal, kemudian session dipertahankan selama koneksi aktif.

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');
      }

      // Menyimpan user data di socket untuk akses selanjutnya
      client.data.user = user;
      client.join(`user:${user.id}`);
    } catch (error) {
      client.emit('error', { message: 'Authentication failed' });
      client.disconnect();
    }
  }
}

Untuk autorisasi pada level message handler, guard dapat digunakan dengan cara yang sama seperti pada 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;
  }
}

Implementasi Room dan Broadcasting

Room merupakan fitur powerful untuk mengelompokkan client dan mengirim pesan ke subset tertentu. Pattern ini sangat berguna untuk fitur seperti chat room, collaborative editing, atau live updates per-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);

    // Notifikasi member lain di 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 ke semua client yang terkoneksi
  broadcastToAll(event: string, payload: unknown): void {
    this.server.emit(event, payload);
  }

  // Broadcast ke user tertentu (semua device/tab)
  broadcastToUser(userId: string, event: string, payload: unknown): void {
    this.server.to(`user:${userId}`).emit(event, payload);
  }
}

Error Handling dan Exception Filters

Penanganan error yang konsisten merupakan aspek penting dalam aplikasi WebSocket. NestJS menyediakan exception filter khusus untuk WebSocket yang dapat di-customize sesuai kebutuhan.

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);
  }
}

Testing WebSocket Gateway

Pengujian gateway memerlukan setup khusus untuk mensimulasikan koneksi WebSocket. NestJS testing utilities dapat dikombinasikan dengan socket.io-client untuk testing komprehensif.

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', () => {
      // Join 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);
    });
  });
});

Siap menguasai wawancara Node.js / NestJS Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Pertanyaan Wawancara Teknis

Berikut adalah pertanyaan-pertanyaan yang sering muncul dalam wawancara teknis terkait NestJS dan WebSockets.

Apa perbedaan antara HTTP request dan WebSocket connection?

HTTP bersifat request-response dengan koneksi yang ditutup setelah respons dikirim. WebSocket mempertahankan koneksi persisten yang memungkinkan komunikasi bidirectional tanpa overhead handshake berulang. WebSocket ideal untuk skenario real-time dimana latensi rendah dan update berkelanjutan dibutuhkan.

Bagaimana NestJS Gateway berbeda dari Controller?

Controller menangani HTTP request dengan pola request-response tradisional. Gateway menangani koneksi WebSocket dengan kemampuan untuk menerima dan mengirim pesan secara asinkron kapan saja selama koneksi aktif. Keduanya terintegrasi dengan sistem dependency injection NestJS.

Kapan menggunakan Socket.IO vs ws library native?

Socket.IO dipilih ketika membutuhkan fitur seperti automatic reconnection, room management, namespace, dan fallback ke long-polling. Library ws native lebih tepat untuk skenario yang memprioritaskan performa maksimal dengan protokol WebSocket standar tanpa abstraksi tambahan.

Bagaimana cara menghandle autentikasi pada WebSocket?

Autentikasi WebSocket dilakukan saat handshake melalui token di header atau query parameter. Token divalidasi di handleConnection lifecycle hook, dan informasi user disimpan di socket.data untuk akses di handler selanjutnya. Guard dapat digunakan untuk autorisasi per-message.

Apa strategi untuk scaling WebSocket server?

Scaling horizontal memerlukan shared state untuk koordinasi antar instance. Redis adapter memungkinkan publish-subscribe antar server sehingga message dapat diteruskan ke client yang terhubung di instance berbeda. Sticky session atau connection draining juga perlu dipertimbangkan.

Bagaimana menangani reconnection di client?

Socket.IO menyediakan automatic reconnection dengan exponential backoff. Aplikasi perlu mengimplementasikan state reconciliation saat reconnect, seperti resync data atau replay missed events. Server dapat menyimpan pending messages untuk client yang disconnect sementara.

Apa perbedaan emit, broadcast, dan to?

Method emit mengirim ke sender socket. Method broadcast.emit mengirim ke semua socket kecuali sender. Method to(room).emit mengirim ke semua socket di room tertentu. Pemahaman perbedaan ini penting untuk implementasi yang tepat.

Kesimpulan

Implementasi WebSocket di NestJS menyediakan abstraksi yang powerful dengan tetap mempertahankan fleksibilitas yang dibutuhkan untuk berbagai use case. Konsep gateway, dikombinasikan dengan fitur-fitur seperti guard, interceptor, dan pipe, memungkinkan pengembang membangun aplikasi real-time yang maintainable dan scalable.

Pemilihan adapter, strategi autentikasi, dan pattern untuk room management merupakan keputusan arsitektur yang perlu disesuaikan dengan kebutuhan spesifik aplikasi. Testing yang komprehensif dan error handling yang robust memastikan reliability aplikasi di production.

Dengan pemahaman mendalam tentang konsep-konsep ini, developer dapat membangun sistem real-time yang efisien dan siap menghadapi tantangan wawancara teknis seputar topik ini.

Anthony Fillion-Maillet

Ditulis oleh

Anthony Fillion-Maillet

Developer fullstack, pendiri SharpSkill

Developer fullstack selama lebih dari 10 tahun. Ia menjalankan SharpSkill dan bertanggung jawab atas semua yang diterbitkan di sini.

Diperbarui 10 Agustus 2026

Tag

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

Bagikan

Artikel terkait