Menambahkan komunikasi realtime ke Express dengan Socket.IO: integrasi http server, event emit dan on, broadcast, rooms, serta membangun realtime chat end-to-end.

Sejauh ini seluruh komunikasi kalian berbasis HTTP request/response: client bertanya, server menjawab. Episode 14 membuka arah lain: WebSocket, di mana server bisa mendorong data ke client tanpa diminta — fondasi chat, notifikasi realtime, kolaborasi, dan dashboard live.
Mengapa Socket.IO, bukan WebSocket murni? Karena Socket.IO membungkus WebSocket dengan fallback (long-polling) untuk jaringan yang memblokir WebSocket, room management, reconnect otomatis, dan event yang mudah dipahami. Di 2026 ia tetap menjadi standar de facto untuk realtime di ekosistem Node.js dan Express.
Socket.IO berjalan di atas HTTP server yang sama dengan Express — keduanya berbagi port. Kuncinya: buat http server dari app, lalu berikan ke Socket.IO:
npm install socket.ioimport { createServer } from "node:http"
import { Server } from "socket.io"
import app from "./app.js"
const httpServer = createServer(app)
const io = new Server(httpServer, {
cors: {
origin: process.env.CORS_ORIGIN?.split(",") || ["http://localhost:5173"],
},
})
io.on("connection", (socket) => {
console.log(`client terhubung: ${socket.id}`)
})
const PORT = process.env.PORT || 3000
httpServer.listen(PORT, () => {
console.log(`Server berjalan di http://localhost:${PORT}`)
})io kini bisa diakses seluruh aplikasi — di episode ini kita menyimpannya di module terpisah agar route Express bisa ikut mengirim event.
Buat src/io.js agar route dan handler bisa memakai instance yang sama:
import { Server } from "socket.io"
export const io = new Server()Lalu di server.js, hubungkan instance bersama itu ke http server:
import { io } from "./io.js"
import { createServer } from "node:http"
const httpServer = createServer(app)
io.attach(httpServer, {
cors: { origin: ["http://localhost:5173"] },
})Pola ini memungkinkan route HTTP (misal POST /messages) memanggil io.emit(...) untuk mengirim event ke client yang tersambung.
Komunikasi Socket.IO berbasis event — emit mengirim, on menerima:
io.on("connection", (socket) => {
console.log(`terhubung: ${socket.id}`)
socket.on("chat:message", (payload) => {
console.log("pesan diterima:", payload)
io.emit("chat:message", {
...payload,
from: socket.id,
at: new Date().toISOString(),
})
})
socket.on("disconnect", () => {
console.log(`terputus: ${socket.id}`)
})
})Alur chat:message:
socket.emit("chat:message", { text }).socket.on("chat:message", handler).io.emit("chat:message", data) — termasuk pengirim.io.emit mengirim ke semua; socket.broadcast.emit mengirim ke semua kecuali pengirim:
socket.on("presence:typing", () => {
socket.broadcast.emit("presence:typing", { user: socket.id })
})Ini pola yang tepat untuk indikator "sedang mengetik" — tidak perlu menginformasikan ke orang yang mengetik.
Room adalah kanal logika di dalam server — cara bersih untuk memisahkan percakapan:
socket.on("room:join", ({ room }) => {
socket.join(room)
socket.to(room).emit("room:joined", { user: socket.id, room })
})
socket.on("room:message", ({ room, text }) => {
io.to(room).emit("room:message", {
room,
text,
from: socket.id,
})
})
socket.on("disconnect", () => {
socket.rooms.forEach((room) => {
socket.to(room).emit("room:left", { user: socket.id })
})
})socket.join(room) memasukkan client ke room.io.to(room).emit(...) mengirim ke semua member room.socket.to(room).emit(...) mengirim ke member room kecuali pengirim.Chat yang baik menyimpan pesan dan melebarkannya ke room:
import { io } from "../io.js"
import { Message } from "../models/Message.js"
io.on("connection", (socket) => {
socket.on("chat:send", async ({ room, text, sender }) => {
const message = await Message.create({ room, text, sender })
io.to(room).emit("chat:message", message)
})
socket.on("chat:history", async ({ room }) => {
const messages = await Message.find({ room })
.sort({ createdAt: 1 })
.limit(50)
socket.emit("chat:history", messages)
})
})Perhatikan pemisahan peran: event chat:send memvalidasi dan menyimpan, lalu melebarkan; event chat:history hanya melayani client yang baru masuk. Validasi body dari episode 13 dipakai juga di sini — misalnya dengan zod di dalam handler chat:send.
Uji dengan client Node mini tanpa browser:
import { io } from "socket.io-client"
const socket = io("http://localhost:3000")
socket.on("connect", () => {
socket.emit("room:join", { room: "express-lab" })
socket.emit("chat:send", {
room: "express-lab",
text: "Halo dari client!",
sender: "user-1",
})
})
socket.on("chat:message", (message) => {
console.log(`${message.sender}: ${message.text}`)
})Jalankan dua instance client dengan sender berbeda — pesan dari satu client akan muncul di client lain dalam hitungan milidetik.
Warning
Jangan pernah percaya socket.id sebagai identitas pengguna untuk keamanan. socket.id berubah setiap reconnect dan bisa dimanipulasi. Untuk chat yang butuh identitas aman, validasi token (pola episode 12) saat handshake dan simpan req.user.id pada objek socket.
Socket.IO mencocokkan nama event persis. Satu typo (chat:send vs chat:sedn) menghasilkan keheningan total. Buat konstanta nama event bersama.
Emit ke socket yang disconnect memicu error socket.io: Write after end. Selalu cek socket.connected atau bungkus dalam try/catch untuk event yang dipicu request HTTP.
io.emit di satu proses hanya menjangkau client yang tersambung ke proses itu. Saat aplikasi diskala horizontal (episode 25), gunakan Redis adapter Socket.IO (@socket.io/redis-adapter) agar event tersebar lintas instance.
Tip
Aturan ergonomis: beri nama event dengan format domain:action (chat:send, presence:typing). Nama yang konsisten membuat codebase realtime mudah dipetakan — pola yang sama dengan penamaan route REST.
Episode 14 membuka kanal komunikasi kedua: Socket.IO terintegrasi dengan http server Express, event emit/on, broadcast, room untuk grup percakapan, dan realtime chat dengan riwayat dari MongoDB.
Inti yang harus dibawa pulang:
createServer(app) + io.attach.io.emit = semua; socket.broadcast.emit = semua kecuali pengirim.io.to(room) menargetkan room; join/leave dikelola per koneksi.socket.id bukan identitas aman — selalu validasi token untuk aksi sensitif.Di episode 15 selanjutnya kita akan menangani upload file & streams — Multer untuk upload, validasi tipe dan ukuran, penyimpanan lokal vs CDN, dan streaming respons file besar. Sampai jumpa di episode 15!