How to Build a Real-Time Chat App with WebSocket from Scratch in 2025

August 14, 2025
6 min read
By Cojocaru David & ChatGPT

Table of Contents

This is a list of all the sections in this post. Click on any of them to jump to that section.

How to Build a Real-Time Chat App with WebSocket from Scratch in 2025

Hey friend, ready to wire up a live chat in under an hour?
Last weekend I promised my cousin I’d show him how Slack-style messaging works under the hood. We built a tiny demo, laughed at our own typos flying across two laptops, and left with a working app. I’m sharing that exact walk-through here.

So grab your coffee. By the end you’ll have:

  • a two-way chat that feels instant
  • clean, copy-paste code
  • next steps for user names, rooms, and real deploys

Sound good? Let’s roll.

Why WebSocket Beats Plain HTTP for Chat

Short version: HTTP is like snail-mail. WebSocket is like a phone call.

With old-school HTTP you ask the server, wait, get an answer, repeat. That’s fine for loading pages. Terrible for chat.

WebSocket opens one wire and keeps it open. The server can push messages the moment they arrive. No polling. No lag. Less battery drain.

Quick wins you’ll notice right away:

  • messages land in ~1 ms on the same network
  • only one TCP connection per user
  • works in every modern browser (even that old iPad at your parents’ house)

What You Need Before We Start

Nothing fancy. If you’ve run console.log('hello') before, you’re set.

  • Node.js 18+ (grab it from nodejs.org)
  • npm or yarn (comes with Node)
  • VS Code (or any editor you like)
  • Chrome or Firefox for testing

That’s it. No paid tools, no cloud accounts yet.

Step 1: Spin Up the WebSocket Server in 5 Minutes

1.1 Create a new folder and hop in

mkdir chat-websocket && cd chat-websocket
npm init -y
npm install ws

1.2 Drop this into server.js

const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
 
wss.on('connection', socket => {
  console.log('👋 new buddy joined');
 
  socket.on('message', raw => {
    const msg = JSON.parse(raw);
    msg.timestamp = Date.now();
 
    // broadcast to everyone except sender
    wss.clients.forEach(client => {
      if (client !== socket && client.readyState === WebSocket.OPEN) {
        client.send(JSON.stringify(msg));
      }
    });
  });
 
  socket.on('close', () => console.log('👋 buddy left'));
});
 
console.log('🚀 server live at ws://localhost:8080');

Run it:

node server.js

You should see the rocket emoji. Keep this terminal open.

Pro tip: We wrapped our text in JSON so later we can add usernames, avatars, or emoji without rewriting everything.

Step 2: Whip Up a Tiny Frontend

Create index.html in the same folder. Paste and save.

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Live Chat</title>
  <style>
    body { font-family: Arial, sans-serif; margin: 2rem; }
    #chat { border: 1px solid #ccc; height: 300px; overflow-y: scroll; padding: 1rem; }
    #msgBox { width: 80%; padding: 0.5rem; }
    button { padding: 0.5rem 1rem; }
  </style>
</head>
<body>
  <h1>💬 Mini Chat</h1>
  <div id="chat"></div>
  <input id="msgBox" placeholder="Type and hit Enter" aria-label="Message input">
  <button onclick="sendMsg()" aria-label="Send message">Send</button>
 
  <script>
    const socket = new WebSocket('ws://localhost:8080');
    const chat = document.getElementById('chat');
 
    socket.onmessage = event => {
      const { text, timestamp } = JSON.parse(event.data);
      const time = new Date(timestamp).toLocaleTimeString();
      chat.innerHTML += `<div><b>${time}</b> ${text}</div>`;
      chat.scrollTop = chat.scrollHeight;
    };
 
    function sendMsg() {
      const input = document.getElementById('msgBox');
      if (!input.value.trim()) return;
      socket.send(JSON.stringify({ text: input.value }));
      input.value = '';
    }
 
    // allow Enter key
    document.getElementById('msgBox').addEventListener('keyup', e => {
      if (e.key === 'Enter') sendMsg();
    });
  </script>
</body>
</html>

Open the file in two browser tabs. Type in one, watch it pop up in the other. Magic!

Step 3: Make It Feel Real (Add Usernames & Colors)

Nobody wants to talk to “Anonymous Hedgehog.” Let’s fix that.

3.1 Prompt for a name on load

Add this right after the body tag in index.html:

const username = prompt('Pick a nickname:') || 'Guest';

3.2 Update the send function

socket.send(JSON.stringify({ text: input.value, username }));

3.3 Update the server broadcast

Change the push line to:

client.send(JSON.stringify({ ...msg, username }));

Refresh your tabs, pick names, and you’ll see something like:

14:32 Ada hey there
14:32 Finn 👋

Step 4: Handle Errors & Reconnects Like a Pro

Real users will close laptops and hop on bad Wi-Fi. Handle it.

Frontend snippet:

socket.onclose = () => {
  chat.innerHTML += '<div><i>Connection lost. Reconnecting...</i></div>';
  setTimeout(() => location.reload(), 2000);
};

Server tip: Ping-pong every 30 s to keep proxies happy.

setInterval(() => {
  wss.clients.forEach(ws => {
    if (ws.isAlive === false) return ws.terminate();
    ws.isAlive = false;
    ws.ping();
  });
}, 30000);
 
wss.on('connection', ws => {
  ws.isAlive = true;
  ws.on('pong', () => ws.isAlive = true);
});

Step 5: Deploy in 3 Clicks (Render Example)

You could stay local forever, but showing off is fun.

  1. Push your code to GitHub.
  2. Go to render.com → New → Web Service.
  3. Pick your repo, set Environment to Node, Build Command to npm install, Start Command to node server.js.
  4. Hit deploy. Done. Render gives you a wss:// URL swap it into the frontend script and you’re live.

Heroku, Railway, or Fly.io? Same idea. Just remember to enable WebSocket support (some free dynos need the --permit-unauthenticated flag).

Common Gotchas & Quick Fixes

ProblemWhy it happensOne-liner fix
Works on localhost, fails on serverforgot HTTPS→WSS switchchange ws:// to wss:// in JS
Messages arrive twicerunning two server instancescheck `ps aux
Browser error “blocked mixed content”serving HTML over HTTPS but trying ws://serve both over HTTPS or use localhost for dev

Stretch Ideas to Level Up

Feeling spicy? Pick one:

  • Rooms: add ?room=gaming query param, filter broadcasts server-side.
  • Typing indicators: send {type:"typing", username} on input events.
  • Message history: dump chats into Redis or Mongo for persistence.
  • Dark mode: one CSS media query and you’re cool again.
  • Rate limiting: max 5 messages/sec to stop spam bots.

Wrapping Up: You Just Built the Internet’s Core Superpower

Real-time chat isn’t just cool it’s the backbone of Slack, Discord, Zoom, and every multiplayer game you love. You now hold the same tech in your hands.

Next step: pick one stretch idea and ship it. Then tweet me the link. I’ll be your first user.

“The best way to learn real-time is to build something real, laugh at the bugs, and hit refresh.” 😊

#WebSocket #RealTimeChat #NodeJSTutorial #SideProject #LearnToCode