This episode builds your first real-time application: a simple chat with a ws server, an HTML UI, join and leave notifications, an online user list, and debugging via DevTools.

Now it is time to tie everything you have learned into one piece: your first real-time application. We build a simple chat with a ws server in Node.js, an HTML interface, an online user list, and join and leave notifications.
Episode 7 is not just about writing code — you also learn to debug a real-time application via DevTools: inspecting WebSocket frames, monitoring the connection state, and tracing messages. By the end of the episode you have an app you can open in several tabs at once and watch messages travel between tabs in real time.
chat-app/
server.js
public/
index.html
style.css
app.jsThe chat-app/server.js + public/ layout above separates server logic from client assets. The server serves static files and WebSocket on the same port — the pattern already introduced in episode 5.
The server needs to know who sent each message. We store the username as a property of the WebSocket object:
const http = require("http");
const fs = require("fs");
const path = require("path");
const { WebSocketServer } = require("ws");
const server = http.createServer((req, res) => {
const file = path.join(__dirname, "public", req.url === "/" ? "index.html" : req.url);
fs.createReadStream(file).pipe(res);
});
const wss = new WebSocketServer({ server });
wss.on("connection", (ws) => {
ws.on("message", (data) => {
const pesan = JSON.parse(data.toString());
if (pesan.type === "join") {
ws.nama = pesan.nama;
broadcast({ type: "sistem", teks: pesan.nama + " bergabung" });
kirimDaftar();
}
if (pesan.type === "chat") {
broadcast({ type: "chat", nama: ws.nama, teks: pesan.teks });
}
});
ws.on("close", () => {
if (ws.nama) {
broadcast({ type: "sistem", teks: ws.nama + " keluar" });
kirimDaftar();
}
});
});Storing ws.nama directly on the connection object keeps the identity attached without an external data structure. All messages are wrapped in JSON with a type field as a marker — the envelope pattern we will dive into in episode 10.
function broadcast(pesan) {
for (const klien of wss.clients) {
klien.send(JSON.stringify(pesan));
}
}
function kirimDaftar() {
const daftar = [];
for (const klien of wss.clients) {
daftar.push(klien.nama);
}
broadcast({ type: "daftar", daftar });
}wss.clients is ws's built-in Set of all active connections — no manual Map needed like in episode 5. broadcast sends objects already serialized with JSON.stringify(pesan).
The chat page has three parts: the user list, the message list, and the send box.
<div id="daftar"></div>
<div id="pesan"></div>
<input id="input" placeholder="ketik pesan">
<button id="kirim">Kirim</button>The structure above separates the online user list, the message list, and the input controls — all populated from JavaScript.
const input = document.getElementById("input");
const ws = new WebSocket("ws://localhost:8080");
const nama = prompt("Nama kamu?");
ws.onopen = () => {
ws.send(JSON.stringify({ type: "join", nama }));
};
ws.onmessage = (event) => {
const pesan = JSON.parse(event.data);
if (pesan.type === "daftar") {
renderDaftar(pesan.daftar);
} else {
renderPesan(pesan);
}
};
function kirim() {
ws.send(JSON.stringify({ type: "chat", teks: input.value }));
input.value = "";
}ws.onopen ensures the join message is sent right after the connection opens. ws.onmessage splits one handler into two paths based on pesan.type.
System notifications such as "user joined" and "user left" come from messages of type sistem sent by the server on the connection and close events. The client simply renders the text with a style distinct from ordinary chat messages.
waktu when wrapping the message, sent along with the payload.typing while typing; the server broadcasts it to the other clients without storing it.Open DevTools in the Network tab, select the WS filter, and click the WebSocket connection. The detail panel shows Messages — every frame sent and received, complete with its timestamp and direction.
out {type: "join", nama: "arman"}
in {type: "sistem", teks: "arman bergabung"}
out {type: "chat", teks: "halo semua"}
in {type: "chat", nama: "arman", teks: "halo semua"}Watching the out and in frame directions above makes the message flow clear. This is the main debugging tool for WebSocket applications — get used to opening it whenever a message does not arrive.
kirimDaftar.Episode 7 brought client and server together into a real chat application: user identity, broadcast, join and leave notifications, an online list, and the ability to debug frames via DevTools.
Key takeaways:
In the next episode we secure this application: authentication and authorization — JWT and session strategies, authentication during the handshake, Socket.IO middleware, room access control, and security best practices.