Building the Server — Express & Socket.IO
The server is the heart of our chat app. It listens for browser connections, places users into rooms, and broadcasts messages to everyone in that room. Create a file named server.js in the project root (same level as package.json).
Why we need three modules
| Module | Purpose |
|---|---|
express | Serves static files (public/index.html, CSS, JS) |
http | Built into Node — creates the HTTP server Express sits on |
socket.io | Upgrades connections to real-time two-way messaging |
app.listen(). You must create http.createServer(app) first, then pass that server to new Server(server).Full server.js — copy and read every comment
Save this complete file. Each section is explained below the code block.
// server.js — Real-time chat backend
// Step 1: Import required modules
const express = require('express'); // Web framework — serves HTML/CSS/JS files
const http = require('http'); // Creates the underlying HTTP server
const { Server } = require('socket.io'); // Socket.IO — real-time two-way communication
// Step 2: Create Express app and HTTP server
const app = express();
const server = http.createServer(app); // Socket.IO needs an HTTP server, not just Express
// Step 3: Attach Socket.IO to the HTTP server
const io = new Server(server);
// Step 4: Serve static files from the "public" folder
// When browser requests /index.html, Express sends public/index.html
app.use(express.static('public'));
// Track online users per room (used in bonus lesson)
const rooms = {};
// Step 5: Handle new WebSocket connections
io.on('connection', (socket) => {
// "socket" = one connected browser tab
console.log('A user connected:', socket.id);
// Event: user joins a named room (e.g. "general", "nodejs-help")
socket.on('join-room', ({ username, room }) => {
socket.username = username; // Save on this socket for later messages
socket.room = room;
socket.join(room); // Socket.IO rooms — like WhatsApp group chats
// Track online users in this room
if (!rooms[room]) rooms[room] = [];
if (!rooms[room].includes(username)) rooms[room].push(username);
// Tell everyone IN the room (not the joiner only) that someone joined
io.to(room).emit('receive-message', {
type: 'system',
message: username + ' joined the room',
timestamp: new Date().toISOString()
});
// Send updated online user list to everyone in the room
io.to(room).emit('online-users', rooms[room]);
});
// Event: user sends a chat message
socket.on('send-message', (message) => {
io.to(socket.room).emit('receive-message', {
type: 'chat',
username: socket.username,
message: message,
timestamp: new Date().toISOString()
});
});
// Event: user is typing (bonus feature)
socket.on('typing', () => {
socket.to(socket.room).emit('user-typing', {
username: socket.username
});
});
// Event: user closes tab or loses connection
socket.on('disconnect', () => {
console.log('User disconnected:', socket.id);
if (socket.room && socket.username) {
// Remove from online list
if (rooms[socket.room]) {
rooms[socket.room] = rooms[socket.room].filter(function (u) {
return u !== socket.username;
});
if (rooms[socket.room].length === 0) delete rooms[socket.room];
}
io.to(socket.room).emit('receive-message', {
type: 'system',
message: socket.username + ' left the room',
timestamp: new Date().toISOString()
});
io.to(socket.room).emit('online-users', rooms[socket.room] || []);
}
});
});
// Step 6: Start the server (Railway uses process.env.PORT)
const PORT = process.env.PORT || 3000;
server.listen(PORT, function () {
console.log('Chat server running on http://localhost:' + PORT);
});
Line-by-line walkthrough
Imports (lines 1–4)
require() loads modules. http is built into Node — no npm install needed. Server from Socket.IO wraps our HTTP server with WebSocket support.
Express + HTTP + Socket.IO setup (lines 6–12)
express.static('public') means: when the browser requests /style.css, send public/style.css. When it requests /, send public/index.html if configured.
app.listen(3000) instead of server.listen(3000). If you listen on app only, Socket.IO may not receive connections correctly.The connection event
io.on('connection', ...) fires every time a browser opens your site and Socket.IO connects. Each connection gets a unique socket.id — like a temporary ID card for that tab.
The join-room event
The client will emit('join-room', { username, room }). We:
- Save username and room on the socket object
- Call
socket.join(room)— Socket.IO’s built-in room feature - Broadcast a system message: “Alex joined the room”
- Send the online users list (bonus feature — already wired here)
The send-message event
When someone sends text, we use io.to(socket.room).emit(...) to push the message to everyone in that room, including the sender. The payload includes username, message text, and ISO timestamp.
The disconnect event
Fires when the user closes the tab or loses internet. We remove them from the online list and notify the room.
Starting the server
process.env.PORT || 3000 uses port 3000 locally, but Railway (Lesson 10) injects its own PORT environment variable.
Quick test (optional now, required in Lesson 7)
npm run dev
You should see: Chat server running on http://localhost:3000. The page will be blank or error until we add HTML — that is expected.
Summary
- Express serves static frontend files from
public/ - Socket.IO listens for custom events:
join-room,send-message,typing,disconnect io.to(room).emitsends data to everyone in a room
Next: build the HTML the user actually sees.