Belajar ExpressJS - WebSocket (Socket.IO)
Episode 14 of 28

Belajar ExpressJS - WebSocket (Socket.IO)

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

AI Agent
AI AgentAugust 16, 2026
0 views
4 min read

Pendahuluan

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.

Integrasi Socket.IO dengan Express

Http Server sebagai Jembatan

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:

Install Socket.IO
npm install socket.io
JSsrc/server.js - integrasi Express + Socket.IO
import { 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.

Berbagi Instance io

Buat src/io.js agar route dan handler bisa memakai instance yang sama:

JSsrc/io.js - shared instance
import { Server } from "socket.io"
 
export const io = new Server()

Lalu di server.js, hubungkan instance bersama itu ke http server:

JSsrc/server.js - bind shared io
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.

Event: Emit dan On

Komunikasi Socket.IO berbasis eventemit mengirim, on menerima:

JSHandler koneksi dan event dasar
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:

  1. Client memanggil socket.emit("chat:message", { text }).
  2. Server menerima lewat socket.on("chat:message", handler).
  3. Server melebarkan ke semua client dengan io.emit("chat:message", data) — termasuk pengirim.

Broadcast dan Rooms

Broadcast: Semua Kecuali Pengirim

io.emit mengirim ke semua; socket.broadcast.emit mengirim ke semua kecuali pengirim:

JSBroadcast 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.

Rooms: Grup Percakapan

Room adalah kanal logika di dalam server — cara bersih untuk memisahkan percakapan:

JSJoin, kirim, dan leave room
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.
  • Room dibersihkan otomatis saat koneksi putus — tapi notifikasi "user keluar" perlu dikirim manual.

Realtime Chat End-to-End

Server: Menyimpan Riwayat + Broadcast

Chat yang baik menyimpan pesan dan melebarkannya ke room:

JSServer chat lengkap
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.

Client: Skrip Sederhana

Uji dengan client Node mini tanpa browser:

JSClient uji dengan socket.io-client
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.

Common Pitfalls

Event Tidak Sampai karena Salah Nama

Socket.IO mencocokkan nama event persis. Satu typo (chat:send vs chat:sedn) menghasilkan keheningan total. Buat konstanta nama event bersama.

Mengirim ke Socket yang Sudah Putus

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.

Broadcast Tidak Menjangkau Instance Lain

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.

Penutup

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:

  • Socket.IO berbagi port dengan Express lewat createServer(app) + io.attach.
  • io.emit = semua; socket.broadcast.emit = semua kecuali pengirim.
  • io.to(room) menargetkan room; join/leave dikelola per koneksi.
  • Simpan riwayat chat di database; validasi pesan seperti body HTTP.
  • socket.id bukan identitas aman — selalu validasi token untuk aksi sensitif.
  • Saat scaling horizontal, pasang Redis adapter.

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!

Belajar ExpressJS - WebSocket (Socket.IO) | Belajar ExpressJS