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.
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.
Nah, untuk kasus real-time kayak chat, notifikasi, atau live dashboard, WebSocket jauh lebih efisien.
Kapan Harus Pakai WebSocket vs HTTP?
| Kebutuhan | HTTP | WebSocket |
|---|---|---|
| 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?
- Fallback otomatis — Kalau browser user nggak support WebSocket (jarang sih sekarang, tapi tetap), Socket.IO bakal fallback ke long-polling secara otomatis.
- Room & Namespace — Socket.IO punya konsep room buat grouping user. Ini super berguna buat chat.
- Auto-reconnect — Koneksi putus? Socket.IO reconnect sendiri.
- Event-based — Lo bisa bikin custom event sesuka hati, kayak
chat_message,user_typing,join_room, dll. - 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
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.onlineUsersMap — 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.