Understanding the Architecture
Before we start coding, let's understand how our real-time chat works:
Components
- Client (React/Next.js): The user interface where users type and see messages
- Socket.IO Server: Handles WebSocket connections and message broadcasting
- Custom Node.js Server: Runs our Next.js app and Socket.IO together
Communication Flow
1User Types Message → Client Socket → Server Socket → All Connected Clients → Display MessageKey Concepts
- WebSocket Connection: A persistent, bidirectional connection between client and server
- Events: Named messages that flow through the WebSocket (e.g., "send-message", "receive-message")
- Broadcasting: Server sends messages to all connected clients simultaneously
Step 1: Project Setup
First, create your Next.js application:
1npx create-next-app@latest <your-project-name>
2cd <your-project-name>Choose the following options when prompted:
- TypeScript: Yes
- ESLint: Yes
- Tailwind CSS: Optional
- App Router: Yes
- src/ directory: Yes
What this does: Sets up a modern Next.js project with TypeScript support, which gives us type safety and better development experience.
Step 2: Installing Dependencies
Install Socket.IO for both server and client:
1npm install socket.io socket.io-clientWhy these packages?
- socket.io: The server-side library that manages WebSocket connections
- socket.io-client: The client-side library that connects to the Socket.IO server
Step 3: Creating the Custom Server
Create a file called `server.js` in your project root:
1const { createServer } = require('http')
2const { parse } = require('url')
3const next = require('next')
4const { Server } = require('socket.io')
5
6const dev = process.env.NODE_ENV !== 'production'
7const hostname = process.env.HOSTNAME || 'localhost'
8const port = parseInt(process.env.PORT || '3000', 10)
9
10const app = next({ dev, hostname, port })
11const handler = app.getRequestHandler()
12
13app.prepare().then(() => {
14 const httpServer = createServer(async (req, res) => {
15 try {
16 const parsedUrl = parse(req.url, true)
17 await handler(req, res, parsedUrl)
18 } catch (err) {
19 console.error('Error occurred handling', req.url, err)
20 res.statusCode = 500
21 res.end('internal server error')
22 }
23 })
24
25 const io = new Server(httpServer, {
26 path: '/api/socket',
27 addTrailingSlash: false,
28 })
29
30 io.on('connection', (socket) => {
31 console.log('Client connected:', socket.id)
32
33 socket.on('send-message', (data) => {
34 console.log('Message received:', data)
35 io.emit('receive-message', data)
36 })
37
38 socket.on('disconnect', () => {
39 console.log('Client disconnected:', socket.id)
40 })
41 })
42
43 httpServer.listen(port, hostname, (err) => {
44 if (err) throw err
45 console.log(`> Ready on http://${hostname}:${port}`)
46 })
47})Understanding this code:
- HTTP Server Creation: We create a custom HTTP server instead of using Next.js's default server
1const httpServer = createServer(async (req, res) => { ... })- Socket.IO Initialization: We attach Socket.IO to our HTTP server
1const io = new Server(httpServer, {
2 path: '/api/socket', // Custom endpoint for WebSocket connections
3})- Connection Event: When a client connects, we get a socket object
1io.on('connection', (socket) => {
2 // socket represents this specific client
3 })- Message Handling: We listen for "send-message" events from clients
1socket.on('send-message', (data) => {
2 // Broadcast to ALL clients including sender
3 io.emit('receive-message', data)
4})Key Point: The `io.emit()` broadcasts to ALL connected clients, creating the real-time effect.
Step 4: Creating the Socket.IO API Route
Create `src/app/api/socket.ts`:
1import { Server as NetServer } from 'http'
2import { NextApiResponse } from 'next'
3import { Server as ServerIO } from 'socket.io'
4
5export const config = {
6 api: {
7 bodyParser: false,
8 },
9}
10
11type NextApiResponseServerIO = NextApiResponse & {
12 socket: {
13 server: NetServer & {
14 io?: ServerIO
15 }
16 }
17}
18
19interface Message {
20 user: string
21 message: string
22 timestamp: number
23}
24
25export default function SocketHandler(req: any, res: NextApiResponseServerIO) {
26 if (!res.socket.server.io) {
27 console.log('Initializing Socket.io...')
28
29 const io = new ServerIO(res.socket.server as any, {
30 path: '/api/socket',
31 addTrailingSlash: false,
32 })
33
34 io.on('connection', (socket) => {
35 console.log('Client connected:', socket.id)
36
37 socket.on('send-message', (data: Message) => {
38 io.emit('receive-message', data)
39 })
40
41 socket.on('disconnect', () => {
42 console.log('Client disconnected:', socket.id)
43 })
44 })
45
46 res.socket.server.io = io
47 } else {
48 console.log('Socket.io already running')
49 }
50
51 res.end()
52}Why this file?
This API route serves as an initialization endpoint. When the client makes a fetch request to `/api/socket`, it ensures the Socket.IO server is initialized and attached to the Next.js server.
The Message Interface: Defines the structure of messages flowing through our app:
1interface Message {
2 user: string // Who sent it
3 message: string // The content
4 timestamp: number // When it was sent
5}Step 5: Building the Chat Component - Part 1: Structure
Create `src/components/Chat/index.tsx` and start with the basic structure:
1'use client'
2
3import { useEffect, useState, useRef } from 'react'
4import { io, Socket } from 'socket.io-client'
5
6interface Message {
7 user: string
8 message: string
9 timestamp: number
10}
11
12const Chat = () => {
13 // State management
14 const [messages, setMessages] = useState<Message[]>([])
15 const [inputMessage, setInputMessage] = useState('')
16 const [username, setUsername] = useState('')
17 const [isConnected, setIsConnected] = useState(false)
18
19 // Refs
20 const socketRef = useRef<Socket | null>(null)
21 const messagesEndRef = useRef<HTMLDivElement>(null)
22
23 // We'll add more code here in next steps
24
25 return <div>Chat Component</div>
26}
27
28export default ChatUnderstanding the State:
- messages: Array of all chat messages
- inputMessage: Current text in the input field
- username: Current user's name
- isConnected: Whether WebSocket is connected
- socketRef: Reference to Socket.IO client instance (persists across re-renders)
- messagesEndRef: Reference to scroll to latest message
Step 6: Building the Chat Component - Part 2: WebSocket Connection
Add the WebSocket initialization inside your Chat component:
1useEffect(() => {
2 const initSocket = async () => {
3 // Initialize the Socket.IO server via API route
4 await fetch('/api/socket')
5
6 // Create Socket.IO client connection
7 socketRef.current = io({
8 path: '/api/socket',
9 })
10
11 // Handle successful connection
12 socketRef.current.on('connect', () => {
13 console.log('Connected to server')
14 setIsConnected(true)
15 })
16
17 // Handle incoming messages
18 socketRef.current.on('receive-message', (data: Message) => {
19 setMessages((prev) => [...prev, data])
20 })
21
22 // Handle disconnection
23 socketRef.current.on('disconnect', () => {
24 console.log('Disconnected from server')
25 setIsConnected(false)
26 })
27 }
28
29 initSocket()
30
31 // Cleanup on unmount
32 return () => {
33 if (socketRef.current) {
34 socketRef.current.disconnect()
35 }
36 }
37}, [])What's happening here:
- Fetch `/api/socket`: Ensures the Socket.IO server is initialized
- Create Socket Client: `io()` creates a connection to our Socket.IO server
- Event Listeners:
- connect: Fires when connection is established
- receive-message: Fires when server broadcasts a new message
- disconnect: Fires when connection is lost
Important: We use `useRef` for the socket so it doesn't get recreated on every render.
Step 7: Building the Chat Component - Part 3: Sending Messages
Add the message sending logic:
1// Auto-scroll to latest message
2useEffect(() => {
3 messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
4}, [messages])
5
6// Send message function
7const sendMessage = (e: React.FormEvent) => {
8 e.preventDefault()
9
10 if (!inputMessage.trim() || !username.trim() || !socketRef.current) return
11
12 const messageData: Message = {
13 user: username,
14 message: inputMessage,
15 timestamp: Date.now(),
16 }
17
18 // Emit event to server
19 socketRef.current.emit('send-message', messageData)
20
21 // Clear input
22 setInputMessage('')
23}The Message Flow:
11. User types message and hits send
22. sendMessage() creates Message object
33. socket.emit('send-message', messageData) sends to server
44. Server receives event and broadcasts with io.emit('receive-message', data)
55. ALL clients (including sender) receive 'receive-message' event
66. Message is added to messages array
77. UI re-renders showing new messageStep 8: Building the Chat Component - Part 4: UI and Styling
First, create a CSS file for styling. Create `src/components/Chat/Chat.module.css`:
1.container {
2 display: flex;
3 justify-content: center;
4 align-items: center;
5 min-height: 100vh;
6 background-color: #0f172a;
7 padding: 20px;
8}
9
10.chatBox {
11 width: 100%;
12 max-width: 800px;
13 height: 600px;
14 background-color: #1e293b;
15 border-radius: 12px;
16 display: flex;
17 flex-direction: column;
18 box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
19}
20
21.header {
22 padding: 20px;
23 border-bottom: 1px solid #334155;
24 display: flex;
25 justify-content: space-between;
26 align-items: center;
27}
28
29.title {
30 margin: 0;
31 color: #f1f5f9;
32 font-size: 24px;
33}
34
35.status {
36 font-size: 14px;
37 font-weight: bold;
38}
39
40.statusConnected {
41 color: #4ade80;
42}
43
44.statusDisconnected {
45 color: #ef4444;
46}
47
48.usernameForm {
49 display: flex;
50 flex-direction: column;
51 justify-content: center;
52 align-items: center;
53 flex: 1;
54 gap: 20px;
55 padding: 40px;
56}
57
58.welcomeText {
59 color: #f1f5f9;
60 font-size: 28px;
61 margin: 0;
62}
63
64.usernameInput {
65 width: 100%;
66 max-width: 300px;
67 padding: 12px;
68 border-radius: 8px;
69 border: 2px solid #334155;
70 background-color: #0f172a;
71 color: #f1f5f9;
72 font-size: 16px;
73 outline: none;
74}
75
76.messagesContainer {
77 flex: 1;
78 overflow-y: auto;
79 padding: 20px;
80 display: flex;
81 flex-direction: column;
82 gap: 12px;
83}
84
85.noMessages {
86 color: #64748b;
87 text-align: center;
88 margin-top: 50px;
89}
90
91.message {
92 padding: 12px;
93 border-radius: 8px;
94 max-width: 70%;
95 word-wrap: break-word;
96}
97
98.messageOwn {
99 background-color: #3b82f6;
100 align-self: flex-end;
101}
102
103.messageOther {
104 background-color: #6b7280;
105 align-self: flex-start;
106}
107
108.messageUser {
109 font-size: 12px;
110 font-weight: bold;
111 margin-bottom: 4px;
112 color: #e2e8f0;
113}
114
115.messageText {
116 font-size: 14px;
117 color: #ffffff;
118 margin-bottom: 4px;
119}
120
121.messageTime {
122 font-size: 10px;
123 color: #cbd5e1;
124 text-align: right;
125}
126
127.form {
128 padding: 20px;
129 border-top: 1px solid #334155;
130 display: flex;
131 gap: 10px;
132}
133
134.input {
135 flex: 1;
136 padding: 12px;
137 border-radius: 8px;
138 border: 2px solid #334155;
139 background-color: #0f172a;
140 color: #f1f5f9;
141 font-size: 14px;
142 outline: none;
143}
144
145.button {
146 padding: 12px 24px;
147 border-radius: 8px;
148 border: none;
149 background-color: #3b82f6;
150 color: white;
151 font-size: 16px;
152 font-weight: bold;
153 cursor: pointer;
154}
155
156.sendButton {
157 padding: 12px 24px;
158 border-radius: 8px;
159 border: none;
160 background-color: #3b82f6;
161 color: white;
162 font-size: 14px;
163 font-weight: bold;
164 cursor: pointer;
165 transition: background-color 0.2s;
166}
167
168.sendButton:hover:not(:disabled) {
169 background-color: #2563eb;
170}
171
172.button:hover {
173 background-color: #2563eb;
174}
175
176.input:disabled,
177.sendButton:disabled {
178 opacity: 0.5;
179 cursor: not-allowed;
180}Now update your Chat component to import and use the CSS module. At the top of `src/components/Chat/index.tsx`, add the import:
1import styles from './Chat.module.css'Then update the JSX return statement to use CSS classes:
1return (
2 <div className={styles.container}>
3 <div className={styles.chatBox}>
4 <div className={styles.header}>
5 <h2 className={styles.title}>Real-Time Chat</h2>
6 <span className={`${styles.status} ${isConnected ? styles.statusConnected : styles.statusDisconnected}`}>
7 {isConnected ? '🟢 Connected' : '🔴 Disconnected'}
8 </span>
9 </div>
10
11 {!username ? (
12 // Username input screen
13 <div className={styles.usernameForm}>
14 <h3 className={styles.welcomeText}>Welcome!</h3>
15 <input
16 type="text"
17 placeholder="Enter your name..."
18 className={styles.usernameInput}
19 onKeyPress={(e) => {
20 if (e.key === 'Enter' && e.currentTarget.value.trim()) {
21 setUsername(e.currentTarget.value)
22 }
23 }}
24 />
25 <button
26 className={styles.button}
27 onClick={(e) => {
28 const input = e.currentTarget.previousElementSibling as HTMLInputElement
29 if (input.value.trim()) {
30 setUsername(input.value)
31 }
32 }}
33 >
34 Join
35 </button>
36 </div>
37 ) : (
38 // Chat interface
39 <>
40 <div className={styles.messagesContainer}>
41 {messages.length === 0 ? (
42 <p className={styles.noMessages}>No messages yet...</p>
43 ) : (
44 messages.map((msg, index) => (
45 <div
46 key={index}
47 className={`${styles.message} ${msg.user === username ? styles.messageOwn : styles.messageOther}`}
48 >
49 <div className={styles.messageUser}>{msg.user}</div>
50 <div className={styles.messageText}>{msg.message}</div>
51 <div className={styles.messageTime}>
52 {new Date(msg.timestamp).toLocaleTimeString([], {
53 hour: '2-digit',
54 minute: '2-digit',
55 })}
56 </div>
57 </div>
58 ))
59 )}
60 <div ref={messagesEndRef} />
61 </div>
62
63 <form onSubmit={sendMessage} className={styles.form}>
64 <input
65 type="text"
66 value={inputMessage}
67 onChange={(e) => setInputMessage(e.target.value)}
68 placeholder="Type your message..."
69 className={styles.input}
70 disabled={!isConnected}
71 />
72 <button
73 type="submit"
74 className={styles.sendButton}
75 disabled={!isConnected || !inputMessage.trim()}
76 >
77 Send
78 </button>
79 </form>
80 </>
81 )}
82 </div>
83 </div>
84)Why use CSS Modules?
Scoped Styles: Class names are automatically scoped to the component, preventing conflicts
Better Organization: Separates styling logic from component logic
Better Performance: CSS is loaded separately and can be cached
Easier Maintenance: Easier to update styles without touching component code
Type Safety: TypeScript can autocomplete class names
UI Features:
Connection Status: Shows if WebSocket is connected
Username Screen: Prompts user to enter name before chatting
Message Alignment: Your messages on right (blue), others on left (gray)
Timestamps: Shows when each message was sent
Auto-scroll: Automatically scrolls to newest message
Step 9: Creating the Main Page
Update `src/app/page.tsx`:
1'use client'
2import React from "react"
3import Chat from "../components/Chat"
4
5export default function Home() {
6 return <Chat />
7}Why 'use client'?: The Chat component uses hooks and WebSocket, which only work in client components, not server components.
Step 10: Configuring Package Scripts
Update your `package.json` scripts:
1"scripts": {
2 "dev": "node server.js",
3 "build": "next build",
4 "start": "NODE_ENV=production node server.js",
5 "lint": "next lint"
6}Why change the scripts?
- We use our custom `server.js` instead of the default Next.js server
- This allows Socket.IO to run alongside Next.js
Run your app:
1npm run devVisit `http://localhost:3000` (or your configured hostname and port) and open multiple browser tabs to test real-time messaging!
Understanding the Data Flow
Let's trace a complete message journey:
1. User Sends Message
1// Chat Component
2const sendMessage = (e: React.FormEvent) => {
3 const messageData = {
4 user: 'User A',
5 message: 'Hello!',
6 timestamp: Date.now()
7 }
8
9 socketRef.current.emit('send-message', messageData)
10}What happens: Client emits event through WebSocket connection
2. Server Receives Message
1socket.on('send-message', (data) => {
2 console.log('Message received:', data)
3 // data = { user: 'User A', message: 'Hello!', timestamp: 1234567890000 }
4
5 // Broadcast to ALL connected clients
6 io.emit('receive-message', data)
7})What happens: Server gets the message and broadcasts it to everyone
3. All Clients Receive Message
1// Chat Component (on all connected clients)
2socketRef.current.on('receive-message', (data: Message) => {
3 setMessages((prev) => [...prev, data])
4})What happens: Every client (including the sender) adds message to their messages array
4. React Re-renders UI
1messages.map((msg, index) => (
2 <div key={index}>
3 <div>{msg.user}</div>
4 <div>{msg.message}</div>
5 </div>
6))What happens: New message appears in everyone's chat simultaneously
Complete Connection Lifecycle
Initial Connection
11. User opens app
22. React component mounts
33. fetch('/api/socket') initializes server
44. io() creates WebSocket connection
55. Server accepts connection and assigns socket.id
66. 'connect' event fires on client
77. isConnected state set to trueSending Messages
11. User types and clicks send
22. socket.emit('send-message', messageData)
33. Data travels through WebSocket to server
44. Server receives 'send-message' event
55. Server broadcasts with io.emit('receive-message', messageData)
66. All connected clients receive 'receive-message' event
77. Messages state updated on all clients
88. React re-renders showing new messageDisconnection
11. User closes tab or loses connection
22. WebSocket connection closes
33. Server detects disconnect
44. 'disconnect' event fires
55. Server logs: "Client disconnected: [socket.id]"Key WebSocket Concepts
Events
Events are named messages that flow through WebSocket connections:
1// Emitting an event (sending)
2socket.emit('event-name', data)
3
4// Listening for an event (receiving)
5socket.on('event-name', (data) => {
6 // Handle received data
7})Broadcasting Patterns
To everyone:
1io.emit('event-name', data) // ALL clients including senderTo everyone except sender:
1socket.broadcast.emit('event-name', data)To specific client:
1io.to(socketId).emit('event-name', data)Rooms (Advanced)
You could extend this app to support multiple chat rooms:
1// Join a room
2socket.join('room-name')
3
4// Send to everyone in a room
5io.to('room-name').emit('event-name', data)Debugging Tips
Check Connection Status
1console.log('Socket connected:', socketRef.current?.connected)
2console.log('Socket ID:', socketRef.current?.id)Monitor Server Events
In `server.js`, add logging:
1io.on('connection', (socket) => {
2 console.log('Total connections:', io.engine.clientsCount)
3
4 socket.onAny((eventName, ...args) => {
5 console.log(`Event: ${eventName}`, args)
6 })
7})Test with Multiple Tabs
Open multiple browser tabs to your application URL (default: `http://localhost:3000`) and send messages between them to verify real-time functionality.
Common Issues and Solutions
Messages not appearing
- Check browser console for connection errors
- Verify Socket.IO server is running (check terminal logs)
- Ensure path matches: `/api/socket` in both client and server
Connection keeps disconnecting
- Check firewall settings
- Verify your configured port is not blocked (default: 3000)
- Look for errors in server console
- Check if another process is using the same port
TypeScript errors
- Ensure Socket.IO types are installed
- Verify all interfaces match between client and server
Next Steps and Improvements
Once you have the basic chat working, consider adding:
- User List: Show who's currently online
- Typing Indicators: Show when someone is typing
- Message History: Store messages in a database
- Private Messages: Direct messages between users
- Rooms/Channels: Multiple chat rooms
- Authentication: User login system
- Message Editing: Edit or delete sent messages
- File Sharing: Send images or files
- Emojis and Formatting: Rich text support
- Read Receipts: Show who's read your messages
Conclusion
You now have a fully functional real-time chat application! The key concepts to remember:
- WebSockets provide persistent, bidirectional communication
- Socket.IO simplifies WebSocket implementation with events
- Broadcasting allows one message to reach all connected clients instantly
- Client and server stay synchronized through emitted events
The beauty of WebSocket is that once connected, data flows instantly without the overhead of HTTP requests, making real-time features feel natural and responsive.
Happy coding! 🚀
