Pernah nggak sih penasaran, gimana sih cara kerja chat di WhatsApp, Telegram, atau Discord? Lo ketik sesuatu, langsung muncul di layar lawan bicara — tanpa refresh, tanpa nunggu. Rasanya kayak sihir, padahal di balik layar ada mekanisme yang cukup elegan bernama WebSocket.

Gue inget pertama kali nyoba bikin fitur chat. Waktu itu gue cuma ngandelin HTTP biasa — polling tiap 2 detik ke server buat ngecek ada pesan baru atau nggak. Hasilnya? Boros resource, lambat, dan user experience-nya jelek banget. Pesan terkirim kayak telat 1-2 detik, belum lagi kalau koneksi lagi jelek.

Lalu gue kenalan sama WebSocket. Dan ya, game-changer banget.

Di artikel ini, gue mau sharing semuanya — dari konsep dasar WebSocket, kenapa lo butuh itu, sampai bikin aplikasi real-time chat dari nol pakai Node.js dan Socket.IO. Gue bakal jelasin step-by-step, lengkap sama contoh kodenya, supaya lo bisa langsung praktik. Anggap aja ini kayak gue nemenin lo ngoding dari sebelah.


Kenapa WebSocket? Kenapa Nggak HTTP Biasa?

Sebelum masuk ke kode, penting banget buat ngerti kenapa kita butuh WebSocket di first place. Soalnya banyak developer junior yang langsung loncat ke library tanpa paham fondasinya.

HTTP itu Satu Arah (Semi-duplex)

HTTP bekerja dengan model request-response. Client kirim request, server balas response. Selesai. Kalau client mau data baru, dia harus kirim request lagi. Ini namanya polling.

CSCSCSleleleiririrevevevnenenetrtrtr::::::""""""ANANAAdidgddahagaa,aplkl1eaaa.sdgag"aaidin?a?2"."b"apreus?a"n."

Bayangin ini terjadi tiap 1-2 detik, dari ratusan user sekaligus. Server lo bakal ngos-ngosan.

WebSocket itu Dua Arah (Full-duplex)

WebSocket bikin koneksi persisten antara client dan server. Begitu koneksi terbuka, keduanya bisa kirim data kapan aja tanpa harus nunggu request. Kayak telepon, bukan surat.

C(CSSClKleelioirrienevveneneentktrrts::::i"(""tHlEOeaahkStln,eeaog,rp!sav"udtetnaeregrrpibpemuusaksaahnk))abs"aiHrhau"lodajruigau!s"erlainnih"

Nah, untuk kasus real-time kayak chat, notifikasi, atau live dashboard, WebSocket jauh lebih efisien.

Kapan Harus Pakai WebSocket vs HTTP?

KebutuhanHTTPWebSocket
Ambil data sekali (API biasa)✅❌ Overkill
Form submit✅❌ Nggak perlu
Real-time chat❌✅
Notifikasi live❌✅
Multiplayer game❌✅
Live streaming data❌✅

Apa Itu Socket.IO dan Kenapa Kita Pakai Itu?

Oke, di sini ada nuansa penting yang sering bikin bingung. WebSocket itu protokol bawaan browser. Socket.IO itu library di atas WebSocket yang bikin hidup lo jauh lebih gampang.

Kenapa nggak pakai WebSocket mentah-mentah aja?

  1. Fallback otomatis — Kalau browser user nggak support WebSocket (jarang sih sekarang, tapi tetap), Socket.IO bakal fallback ke long-polling secara otomatis.
  2. Room & Namespace — Socket.IO punya konsep room buat grouping user. Ini super berguna buat chat.
  3. Auto-reconnect — Koneksi putus? Socket.IO reconnect sendiri.
  4. Event-based — Lo bisa bikin custom event sesuka hati, kayak chat_message, user_typing, join_room, dll.
  5. Broadcast — Kirim pesan ke semua user, atau ke room tertentu, tinggal satu baris kode.

Jadi ya, buat production, pakai Socket.IO itu no-brainer.


Setup Project: Mulai dari Nol

Sekarang kita mulai bikin. Lo butuh:

  • Node.js (v18 ke atas recommended)
  • npm atau yarn
  • Text editor (VS Code recommended)
  • Browser modern (Chrome, Firefox, dll.)

Inisialisasi Project

Buat folder baru dan inisialisasi npm:

mkdir realtime-chat
cd realtime-chat
npm init -y

Install dependencies yang kita butuhkan:

npm install express socket.io
npm install -D nodemon
  • express — web framework buat serve file statis dan HTTP server
  • socket.io — library WebSocket
  • nodemon — auto-restart server tiap ada perubahan kode (biar nggak manual restart terus)

Edit package.json, tambah script:

{
  "name": "realtime-chat",
  "version": "1.0.0",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  }
}

Struktur Folder

r├│││├└e───a───ltp├├└spiu───eamb───rcelvk-iisceaccntlrgh/dyi.eaelej.txensj/..tshc.otsjnmssl

Membuat Server Node.js dengan Socket.IO

Ini jantung aplikasi kita. Buat file server.js:

const express = require('express');
const http = require('http');
const { Server } = require('socket.io');

const app = express();
const server = http.createServer(app);
const io = new Server(server);

// Serve file statis dari folder public
app.use(express.static('public'));

// Simpan user yang sedang online
const onlineUsers = new Map();

io.on('connection', (socket) => {
  console.log(`User terhubung: ${socket.id}`);

  // Event: user join ke chat room
  socket.on('join_room', (data) => {
    const { username, room } = data;
    
    socket.join(room);
    onlineUsers.set(socket.id, { username, room });

    // Notifikasi ke semua user di room bahwa ada yang join
    socket.to(room).emit('receive_message', {
      username: 'System',
      message: `${username} telah bergabung ke room 🎉`,
      timestamp: new Date().toISOString(),
      type: 'system'
    });

    // Kirim daftar user online ke room
    const roomUsers = [];
    onlineUsers.forEach((user) => {
      if (user.room === room) {
        roomUsers.push(user.username);
      }
    });
    io.to(room).emit('online_users', roomUsers);

    console.log(`${username} join room: ${room}`);
  });

  // Event: kirim pesan
  socket.on('send_message', (data) => {
    const user = onlineUsers.get(socket.id);
    if (user) {
      io.to(user.room).emit('receive_message', {
        username: user.username,
        message: data.message,
        timestamp: new Date().toISOString(),
        type: 'user'
      });
    }
  });

  // Event: user sedang mengetik
  socket.on('typing', () => {
    const user = onlineUsers.get(socket.id);
    if (user) {
      socket.to(user.room).emit('user_typing', {
        username: user.username
      });
    }
  });

  // Event: user berhenti mengetik
  socket.on('stop_typing', () => {
    const user = onlineUsers.get(socket.id);
    if (user) {
      socket.to(user.room).emit('user_stop_typing', {
        username: user.username
      });
    }
  });

  // Event: user disconnect
  socket.on('disconnect', () => {
    const user = onlineUsers.get(socket.id);
    if (user) {
      socket.to(user.room).emit('receive_message', {
        username: 'System',
        message: `${user.username} telah keluar 😢`,
        timestamp: new Date().toISOString(),
        type: 'system'
      });

      // Update daftar user online
      onlineUsers.delete(socket.id);
      const roomUsers = [];
      onlineUsers.forEach((u) => {
        if (u.room === user.room) {
          roomUsers.push(u.username);
        }
      });
      io.to(user.room).emit('online_users', roomUsers);
    }

    console.log(`User terputus: ${socket.id}`);
  });
});

const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
  console.log(`Server berjalan di http://localhost:${PORT}`);
});

Ada beberapa hal yang gue mau highlight di sini:

  • io.on('connection') — Ini trigger setiap kali ada client yang terhubung.
  • socket.join(room) — Socket.IO punya fitur room. User bisa masuk ke room tertentu, dan pesan hanya dikirim ke user di room tersebut.
  • socket.to(room).emit() — Kirim event ke semua user di room KECUALE pengirim.
  • io.to(room).emit() — Kirim event ke SEMUA user di room termasuk pengirim.
  • onlineUsers Map — Kita simpan data user di memory. Di production, lo bisa pakai Redis.

Membuat Tampilan Frontend (HTML & CSS)

Buat file public/index.html:

<!DOCTYPE html>
<html lang="id">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Real-time Chat - Node.js & Socket.IO</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <!-- Halaman Join -->
  <div id="join-screen" class="screen active">
    <div class="join-container">
      <h1>💬 Real-time Chat</h1>
      <p>Masukkan nama dan pilih room untuk mulai ngobrol</p>
      <input type="text" id="username-input" placeholder="Nama kamu..." maxlength="20">
      <select id="room-select">
        <option value="general">🌐 General</option>
        <option value="tech">💻 Tech</option>
        <option value="gaming">🎮 Gaming</option>
        <option value="random">🎲 Random</option>
      </select>
      <button id="join-btn">Gabung Chat</button>
    </div>
  </div>

  <!-- Halaman Chat -->
  <div id="chat-screen" class="screen">
    <div class="chat-container">
      <div class="chat-header">
        <div class="header-left">
          <h2 id="room-name">Room: General</h2>
          <span id="online-count">0 online</span>
        </div>
        <button id="leave-btn">Keluar</button>
      </div>

      <div class="chat-body">
        <div id="messages" class="messages"></div>
        <div id="online-panel" class="online-panel">
          <h3>Online</h3>
          <ul id="online-list"></ul>
        </div>
      </div>

      <div id="typing-indicator" class="typing-indicator"></div>

      <div class="chat-footer">
        <input type="text" id="message-input" placeholder="Ketik pesan..." maxlength="500">
        <button id="send-btn">Kirim</button>
      </div>
    </div>
  </div>

  <script src="/socket.io/socket.io.js"></script>
  <script src="client.js"></script>
</body>
</html>

Buat file public/style.css:

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  background: #1a1a2e;
  color: #eee;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
}

.screen { display: none; }
.screen.active { display: flex; }

/* Join Screen */
.join-container {
  background: #16213e;
  padding: 40px;
  border-radius: 16px;
  text-align: center;
  width: 400px;
  box-shadow: 0 8px 32px rgba(0,0,0,0.3);
}

.join-container h1 {
  font-size: 2rem;
  margin-bottom: 8px;
}

.join-container p {
  color: #aaa;
  margin-bottom: 24px;
}

.join-container input,
.join-container select {
  width: 100%;
  padding: 12px 16px;
  margin-bottom: 12px;
  border: 2px solid #0f3460;
  border-radius: 8px;
  background: #1a1a2e;
  color: #eee;
  font-size: 1rem;
  outline: none;
}

.join-container input:focus,
.join-container select:focus {
  border-color: #e94560;
}

.join-container button {
  width: 100%;
  padding: 14px;
  background: #e94560;
  color: white;
  border: none;
  border-radius: 8px;
  font-size: 1.1rem;
  cursor: pointer;
  transition: background 0.3s;
}

.join-container button:hover {
  background: #c73e54;
}

/* Chat Screen */
.chat-container {
  width: 900px;
  height: 600px;
  background: #16213e;
  border-radius: 16px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  box-shadow: 0 8px 32px rgba(0,0,0,0.3);
}

.chat-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px 20px;
  background: #0f3460;
}

.header-left h2 { font-size: 1.1rem; }
#online-count { font-size: 0.85rem; color: #aaa; }

#leave-btn {
  padding: 8px 16px;
  background: #e94560;
  color: white;
  border: none;
  border-radius: 6px;
  cursor: pointer;
}

.chat-body {
  flex: 1;
  display: flex;
  overflow: hidden;
}

.messages {
  flex: 1;
  padding: 16px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.online-panel {
  width: 180px;
  background: #0f3460;
  padding: 16px;
  overflow-y: auto;
}

.online-panel h3 {
  font-size: 0.9rem;
  margin-bottom: 12px;
  color: #aaa;
}

.online-panel ul {
  list-style: none;
}

.online-panel li {
  padding: 6px 0;
  font-size: 0.9rem;
  border-bottom: 1px solid rgba(255,255,255,0.05);
}

.message {
  max-width: 70%;
  padding: 10px 14px;
  border-radius: 12px;
  font-size: 0.95rem;
  line-height: 1.4;
}

.message.own {
  align-self: flex-end;
  background: #e94560;
  border-bottom-right-radius: 4px;
}

.message.other {
  align-self: flex-start;
  background: #1a1a2e;
  border-bottom-left-radius: 4px;
}

.message.system {
  align-self: center;
  background: transparent;
  color: #888;
  font-size: 0.8rem;
  font-style: italic;
}

.message .meta {
  font-size: 0.75rem;
  color: rgba(255,255,255,0.6);
  margin-top: 4px;
}

.typing-indicator {
  padding: 4px 20px;
  font-size: 0.8rem;
  color: #aaa;
  min-height: 24px;
}

.chat-footer {
  display: flex;
  padding: 12px 16px;
  background: #0f3460;
  gap: 8px;
}

.chat-footer input {
  flex: 1;
  padding: 10px 16px;
  border: 2px solid transparent;
  border-radius: 8px;
  background: #1a1a2e;
  color: #eee;
  font-size: 0.95rem;
  outline: none;
}

.chat-footer input:focus {
  border-color: #e94560;
}

.chat-footer button {
  padding: 10px 24px;
  background: #e94560;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-size: 0.95rem;
}

Membuat Client-side JavaScript

Ini bagian yang bikin frontend “hidup”. Buat file public/client.js:

const socket = io();

// DOM Elements
const joinScreen = document.getElementById('join-screen');
const chatScreen = document.getElementById('chat-screen');
const usernameInput = document.getElementById('username-input');
const roomSelect = document.getElementById('room-select');
const joinBtn = document.getElementById('join-btn');
const leaveBtn = document.getElementById('leave-btn');
const messageInput = document.getElementById('message-input');
const sendBtn = document.getElementById('send-btn');
const messagesDiv = document.getElementById('messages');
const roomName = document.getElementById('room-name');
const onlineCount = document.getElementById('online-count');
const onlineList = document.getElementById('online-list');
const typingIndicator = document.getElementById('typing-indicator');

let currentUsername = '';
let currentRoom = '';
let typingTimeout = null;

// Join ke room
joinBtn.addEventListener('click', () => {
  const username = usernameInput.value.trim();
  const room = roomSelect.value;

  if (!username) {
    alert('Masukkan nama dulu ya!');
    return;
  }

  currentUsername = username;
  currentRoom = room;

  socket.emit('join_room', { username, room });

  joinScreen.classList.remove('active');
  chatScreen.classList.add('active');
  roomName.textContent = `Room: ${roomSelect.options[roomSelect.selectedIndex].text}`;
});

// Support Enter di input username
usernameInput.addEventListener('keypress', (e) => {
  if (e.key === 'Enter') joinBtn.click();
});

// Kirim pesan
function sendMessage() {
  const message = messageInput.value.trim();
  if (!message) return;

  socket.emit('send_message', { message });
  messageInput.value = '';
  socket.emit('stop_typing');
}

sendBtn.addEventListener('click', sendMessage);

messageInput.addEventListener('keypress', (e) => {
  if (e.key === 'Enter') {
    sendMessage();
    return;
  }

  // Typing indicator
  socket.emit('typing');
  clearTimeout(typingTimeout);
  typingTimeout = setTimeout(() => {
    socket.emit('stop_typing');
  }, 1000);
});

// Terima pesan
socket.on('receive_message', (data) => {
  const div = document.createElement('div');
  div.classList.add('message');

  if (data.type === 'system') {
    div.classList.add('system');
    div.textContent = data.message;
  } else if (data.username === currentUsername) {
    div.classList.add('own');
    div.innerHTML = `
      <div>${escapeHtml(data.message)}</div>
      <div class="meta">${data.username} · ${formatTime(data.timestamp)}</div>
    `;
  } else {
    div.classList.add('other');
    div.innerHTML = `
      <div>${escapeHtml(data.message)}</div>
      <div class="meta">${data.username} · ${formatTime(data.timestamp)}</div>
    `;
  }

  messagesDiv.appendChild(div);
  messagesDiv.scrollTop = messagesDiv.scrollHeight;
});

// Daftar user online
socket.on('online_users', (users) => {
  onlineCount.textContent = `${users.length} online`;
  onlineList.innerHTML = users
    .map((u) => `<li>${escapeHtml(u)}</li>`)
    .join('');
});

// Typing indicator
let typingUsers = new Set();

socket.on('user_typing', (data) => {
  typingUsers.add(data.username);
  updateTypingIndicator();
});

socket.on('user_stop_typing', (data) => {
  typingUsers.delete(data.username);
  updateTypingIndicator();
});

function updateTypingIndicator() {
  if (typingUsers.size === 0) {
    typingIndicator.textContent = '';
  } else if (typingUsers.size === 1) {
    typingIndicator.textContent = `${[...typingUsers][0]} sedang mengetik...`;
  } else {
    typingIndicator.textContent = `${typingUsers.size} orang sedang mengetik...`;
  }
}

// Keluar dari room
leaveBtn.addEventListener('click', () => {
  location.reload();
});

// Utility functions
function formatTime(isoString) {
  const date = new Date(isoString);
  return date.toLocaleTimeString('id-ID', {
    hour: '2-digit',
    minute: '2-digit'
  });
}

function escapeHtml(text) {
  const div = document.createElement('div');
  div.textContent = text;
  return div.innerHTML;
}

Perhatikan fungsi escapeHtml — ini penting banget buat mencegah XSS (Cross-Site Scripting). Jangan pernah render user input langsung ke innerHTML tanpa sanitize. Trust nobody.


Jalankan dan Tes

Sekarang saatnya nge-run:

npm run dev

Buka browser di http://localhost:3000. Buka 2 tab (atau 2 browser berbeda), masuk ke room yang sama, dan mulai chat.

Lo bakal langsung ngerasain bedanya — pesan muncul instan, tanpa delay, tanpa refresh. Itulah kekuatan WebSocket.


Fitur yang Bisa Lo Tambahkan

Project ini masih basic. Tapi fondasinya udah kuat. Berikut beberapa fitur yang bisa lo kembangkan:

1. Persistensi Pesan dengan Database

Sekarang pesan hilang kalau user refresh. Lo bisa simpan ke MongoDB atau PostgreSQL:

// Contoh sederhana dengan MongoDB
const mongoose = require('mongoose');

const messageSchema = new mongoose.Schema({
  username: String,
  message: String,
  room: String,
  timestamp: { type: Date, default: Date.now }
});

const Message = mongoose.model('Message', messageSchema);

// Di dalam event send_message:
socket.on('send_message', async (data) => {
  const user = onlineUsers.get(socket.id);
  if (user) {
    const newMessage = new Message({
      username: user.username,
      message: data.message,
      room: user.room
    });
    await newMessage.save();

    io.to(user.room).emit('receive_message', {
      username: user.username,
      message: data.message,
      timestamp: new Date().toISOString(),
      type: 'user'
    });
  }
});

// Load history saat join
socket.on('join_room', async (data) => {
  // ... kode join yang sudah ada ...

  // Kirim 50 pesan terakhir
  const history = await Message.find({ room })
    .sort({ timestamp: -1 })
    .limit(50)
    .lean();

  socket.emit('chat_history', history.reverse());
});

2. Autentikasi User

Pakai JWT (JSON Web Token) buat autentikasi:

const jwt = require('jsonwebtoken');

// Middleware Socket.IO
io.use((socket, next) => {
  const token = socket.handshake.auth.token;
  try {
    const decoded = jwt.verify(token, 'SECRET_KEY');
    socket.user = decoded;
    next();
  } catch (err) {
    next(new Error('Autentikasi gagal'));
  }
});

3. Private Message (DM)

// Kirim private message
socket.on('private_message', (data) => {
  const { to, message } = data;
  
  // Cari socket ID target
  let targetSocketId = null;
  onlineUsers.forEach((user, socketId) => {
    if (user.username === to) targetSocketId = socketId;
  });

  if (targetSocketId) {
    io.to(targetSocketId).emit('receive_private_message', {
      from: onlineUsers.get(socket.id).username,
      message,
      timestamp: new Date().toISOString()
    });
  }
});

4. File Sharing

Socket.IO juga support binary data. Lo bisa kirim file kecil lewat event:

// Client
const fileInput = document.getElementById('file-input');
fileInput.addEventListener('change', (e) => {
  const file = e.target.files[0];
  const reader = new FileReader();
  reader.onload = (e) => {
    socket.emit('file_share', {
      name: file.name,
      type: file.type,
      size: file.size,
      data: e.target.result
    });
  };
  reader.readAsDataURL(file);
});

Penutup

Nah, sekarang lo udah bisa bikin real-time chat sendiri dari nol pake Node.js, Express, dan Socket.IO. Mulai dari server, frontend, sampe fitur kayak typing indicator, online status, room chat, dan file sharing. Keren kan?

WebSocket bukan cuma buat chat — lo bisa terapin ke notifikasi real-time, live dashboard, kolaborasi dokumen, atau game online. Pokoknya apapun yang butuh komunikasi dua arah secara real-time.

Kalau lo ada pertanyaan atau pengalaman pas ngikutin tutorial ini, langsung aja email ke [email protected]. Happy coding!

FAQ

Q: Apa bedanya WebSocket dengan Socket.IO? A: WebSocket adalah protokol komunikasi dua arah, sedangkan Socket.IO adalah library yang membungkus WebSocket dengan fitur tambahan kayak auto-reconnect, fallback transport, dan room management. Socket.IO lebih mudah dipakai untuk production.

Q: Apakah Socket.IO aman untuk production? A: Ya, selama lo pake HTTPS dan konfigurasi CORS dengan benar. Jangan lupa validasi input user dan batasi rate koneksi biar server nggak kena DDOS.

Q: Berapa maksimal koneksi WebSocket yang bisa ditangani satu server? A: Tergantung spek server. Dengan Node.js, satu instance bisa handle ribuan koneksi simultan karena event-driven. Untuk skala besar, lo perlu pake horizontal scaling dengan Redis adapter.

Q: Apakah kode ini bisa jalan di hosting gratis seperti Railway atau Fly.io? A: Bisa, selama hostingnya support WebSocket. Railway dan Fly.io support WebSocket out of the box. Pastikan setelan firewall lo nggak ngeblock port WebSocket.