Real-time communication is no longer a “nice-to-have” — it’s a baseline expectation. Whether you’re building a customer support widget, a collaborative workspace tool, or an internal team messenger, users expect messages to appear instantly. No refresh. No delay. Just live.
In this post, I’m walking you through how to build a fully functional real-time chat application using React on the frontend, Node.js with Express on the backend, and Socket.io to handle the real-time magic in between. This is the exact kind of project I include in my React Node.js developer portfolio to demonstrate full-stack capability to clients — and by the end of this guide, you’ll understand why it’s such a powerful showcase piece.
Let’s explore what goes into building something that actually works in production, not just in a tutorial. This is also one of the strongest additions you can make to a React Node.js developer portfolio.
Why Real-Time Apps Belong in Your Developer Portfolio
Before we write a single line of code, let me explain why a real-time chat project is one of the most effective things you can put in your Node.js developer portfolio.
Most CRUD apps look the same to potential clients. A to-do list, a blog backend, a basic REST API — they all demonstrate similar skill levels on the surface. But a real-time chat app? That’s a different story. It signals that you understand:
- Persistent connections (WebSockets vs. HTTP polling)
- Event-driven architecture on the server side
- Frontend state management under rapidly changing data
- Concurrency handling — what happens when 50 users send messages at once?
I, Usman Nadeem, have shipped several real-time features in commercial projects — from live order tracking dashboards in a POS system to collaborative editing tools in a SaaS platform. Every one of those projects started with the same foundational understanding you’ll build here.
The Tech Stack Breakdown
Here’s what we’re working with and why each piece earns its place:
| Layer | Technology | Why It’s Used |
|---|---|---|
| Frontend | React (with hooks) | Component-based UI, efficient re-renders |
| Backend | Node.js + Express | Non-blocking I/O, lightweight HTTP server |
| Real-time layer | Socket.io | Abstracted WebSocket API with fallbacks |
| State management | React useState / useEffect | Local state for chat messages and connection |
| Transport protocol | WebSocket (via Socket.io) | Full-duplex communication over a single connection |
Every tool in this stack pulls its weight — and together they form the backbone of a standout React Node.js developer portfolio project. Socket.io is worth a special mention here. While you could use raw WebSockets, Socket.io gives you automatic reconnection, room management, event namespacing, and graceful fallback to HTTP long-polling for older clients. You can explore the full API in the official Socket.io v4 documentation. For a Socket.io React real-time app, it’s the industry-standard choice for good reason.
Project Setup
Prerequisites
Make sure you have the following installed:
- Node.js v18+ and npm
- A basic understanding of React and Express
- A code editor (VS Code recommended)
Folder Structure
/chat-app
/client ← React frontend
/server ← Node.js + Express + Socket.io backendWe’ll keep the frontend and backend as separate folders — this mirrors how you’d structure a real production project, and it’s something clients notice when reviewing your work.
Building the Node.js Backend
Step 1: Initialize the Server
mkdir server && cd server
npm init -y
npm install express socket.io corsStep 2: Create index.js
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const cors = require('cors');
const app = express();
app.use(cors());
const server = http.createServer(app);
const io = new Server(server, {
cors: {
origin: "http://localhost:3000",
methods: ["GET", "POST"]
}
});
const users = {};
io.on('connection', (socket) => {
console.log(`User connected: ${socket.id}`);
socket.on('set_username', (username) => {
users[socket.id] = username;
io.emit('user_joined', { username, id: socket.id });
});
socket.on('send_message', (data) => {
io.emit('receive_message', {
text: data.text,
sender: users[socket.id] || 'Anonymous',
timestamp: new Date().toLocaleTimeString()
});
});
socket.on('disconnect', () => {
const username = users[socket.id];
delete users[socket.id];
if (username) {
io.emit('user_left', { username });
}
console.log(`User disconnected: ${socket.id}`);
});
});
server.listen(4000, () => {
console.log('Server running on port 4000');
});A few things worth noting here:
- We’re using
http.createServer(app)instead ofapp.listen()— Socket.io needs access to the raw HTTP server. io.emit()broadcasts to all connected clients;socket.emit()sends only to the sender.- The
usersobject is an in-memory map of socket IDs to usernames. In a production app, you’d replace this with a database.
Pro tip: Socket.io’s built-in room system lets you create private chat rooms by calling
socket.join('room-name'). This is how you’d extend a basic chat into a multi-channel Slack-like app.
Building the React Frontend
Step 1: Bootstrap the React App
npx create-react-app client
cd client
npm install socket.io-clientStep 2: Create the Chat Component
// src/App.js
import React, { useState, useEffect, useRef } from 'react';
import { io } from 'socket.io-client';
const socket = io('http://localhost:4000');
function App() {
const [username, setUsername] = useState('');
const [joined, setJoined] = useState(false);
const [message, setMessage] = useState('');
const [messages, setMessages] = useState([]);
const [notification, setNotification] = useState('');
const messagesEndRef = useRef(null);
useEffect(() => {
socket.on('receive_message', (data) => {
setMessages((prev) => [...prev, data]);
});
socket.on('user_joined', ({ username }) => {
setNotification(`${username} joined the chat`);
setTimeout(() => setNotification(''), 3000);
});
socket.on('user_left', ({ username }) => {
setNotification(`${username} left the chat`);
setTimeout(() => setNotification(''), 3000);
});
return () => {
socket.off('receive_message');
socket.off('user_joined');
socket.off('user_left');
};
}, []);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const handleJoin = () => {
if (username.trim()) {
socket.emit('set_username', username);
setJoined(true);
}
};
const handleSend = () => {
if (message.trim()) {
socket.emit('send_message', { text: message });
setMessage('');
}
};
if (!joined) {
return (
<div className="join-screen">
<h2>Join Chat</h2>
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Enter your name"
onKeyDown={(e) => e.key === 'Enter' && handleJoin()}
/>
<button onClick={handleJoin}>Join</button>
</div>
);
}
return (
<div className="chat-container">
<div className="messages">
{notification && <div className="notification">{notification}</div>}
{messages.map((msg, index) => (
<div key={index} className={`message ${msg.sender === username ? 'own' : 'other'}`}>
<span className="sender">{msg.sender}</span>
<p>{msg.text}</p>
<span className="timestamp">{msg.timestamp}</span>
</div>
))}
<div ref={messagesEndRef} />
</div>
<div className="input-area">
<input
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Type a message..."
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
/>
<button onClick={handleSend}>Send</button>
</div>
</div>
);
}
export default App;There are a few patterns here I want to highlight explicitly, because they come up in real client projects:
Cleanup on unmount: The return inside useEffect calls socket.off() — this prevents memory leaks and duplicate event listeners if the component remounts.
Optimistic scrolling: The second useEffect auto-scrolls to the latest message using a ref. Small detail, but it makes the UX feel polished.
Conditional rendering: The join screen and chat screen are cleanly separated in state — no page navigation needed.
How Socket.io’s Event System Works
If you’re new to Socket.io, here’s a mental model that makes everything click: think of it like a radio broadcast system with named channels.
socket.emit('event_name', data)— You transmit on a channelsocket.on('event_name', callback)— You tune in to a channelio.emit()— Server broadcasts to every connected radio at oncesocket.to('room').emit()— Server broadcasts to everyone in a specific room
This event-driven model is fundamentally different from REST APIs. In REST, the client always initiates. With Socket.io, the server can push data to the client at any time — that’s the key insight that makes real-time features possible.
According to freelance developer Usman Nadeem, the most common mistake junior developers make with Socket.io is registering event listeners inside render loops — always define your socket listeners in a useEffect with an empty dependency array to ensure they only register once.
Taking It Further: Production Considerations
This tutorial gets you to a working local prototype. But a polished, deployed version is exactly what elevates a React Node.js developer portfolio from average to impressive. But if you’re showing this off in a React Node.js developer portfolio or pitching it to a client, here’s what you’d add for a production-ready version:
Authentication & User Sessions
- Integrate JWT tokens passed during the Socket.io handshake
- Validate users on the server before allowing them to join rooms
Message Persistence
- Store messages in MongoDB or PostgreSQL
- Send the last N messages to new users on connection via
socket.emit('message_history', pastMessages)
Typing Indicators
// Client: emit while typing
socket.emit('typing', { username });
// Server: broadcast to others
socket.on('typing', (data) => {
socket.broadcast.emit('show_typing', data);
});Horizontal Scaling
- When you run multiple Node.js instances, Socket.io needs a shared adapter
- Use
socket.io-redisorsocket.io-mongoto sync events across instances. The Socket.io multiple nodes guide covers this setup in detail.
Deployment
- Deploy the Node.js server to Railway, Render, or a VPS
- Deploy the React frontend to Vercel or Netlify
- Use environment variables to manage socket server URLs
Common Mistakes to Avoid
Even experienced developers hit these when building Socket.io React real-time apps for the first time. Avoiding them is what separates a professional React Node.js developer portfolio piece from a rushed tutorial clone:
- Not cleaning up event listeners — Leads to duplicate messages appearing when the component re-renders
- Using
socket.idas a permanent user identifier — Socket IDs change on reconnection; always use a stable user ID from your auth system - Broadcasting to the sender — Use
socket.broadcast.emit()when you want to notify everyone except the sender - Forgetting CORS configuration — The Socket.io server needs explicit CORS headers for cross-origin browser connections
- No rate limiting — Without message throttling, a single abusive client can flood the server
Conclusion
Building a real-time chat app is one of those projects that teaches you more than almost anything else. You learn about persistent connections, server-side event handling, frontend state management under unpredictable data flow, and how to think about scale from day one.
I, Usman Nadeem, have built real-time features across SaaS products, fintech dashboards, and custom POS systems — and the underlying principles from this tutorial show up in every one of them. WebSocket-based communication, event-driven server design, and clean React state patterns are not just portfolio skills. They’re production skills.
If you’re looking to add this to your own portfolio, don’t stop at the basic version. Add authentication, rooms, and message history. Deploy it somewhere public. Let potential clients or employers actually use it — nothing is more convincing than a live demo.
If you need help building or extending a real-time feature for your application, you can reach out to me via my portfolio at usmannadeem.com. You can also explore my other technical write-ups on building REST APIs with Node.js and choosing the right full-stack framework for your next project.
FAQ
Q1: Can I build a real-time chat app without Socket.io?
Yes — you can use native WebSockets directly, or Server-Sent Events (SSE) for one-directional data push. However, Socket.io is recommended for most production use cases because it handles reconnection logic, room management, and cross-browser compatibility automatically. It’s also the go-to choice for any React Node.js developer portfolio project involving real-time features. The overhead it adds is minimal compared to the features you get.
Q2: How many simultaneous connections can a Node.js + Socket.io server handle?
A single Node.js process can typically handle 10,000–100,000+ concurrent WebSocket connections depending on message frequency and server resources. For apps expecting heavy load, you’d scale horizontally with multiple Node instances behind a load balancer and use a Redis adapter to share socket state across instances.
Q3: Is this approach suitable for mobile apps?
Absolutely. Socket.io has official client libraries for React Native, Android (Java/Kotlin), and iOS (Swift). The server code stays exactly the same — you’d just swap the React web client for a React Native one.
Q4: Should I use Socket.io or GraphQL Subscriptions for real-time features?
If your entire API is already built on GraphQL (e.g., Apollo Server), GraphQL Subscriptions can be a clean choice as they keep everything in one schema. For everything else — especially when you need fine-grained event control, custom rooms, or non-query-shaped events — Socket.io is more flexible and lower latency.
Q5: How do I add private one-on-one messaging?
Use Socket.io rooms. When two users want a private conversation, create a room with a deterministic ID (e.g., sorted combination of their user IDs). Have both users join that room, then use io.to(roomId).emit() to send messages only to that pair. This scales cleanly and avoids storing room IDs in a database for simple cases.

