Building a Realtime Chat App with React, Node.js, and PostgreSQL
Introduction
In today's interactive world, real-time communication thrives. This article guides you through building a basic real-time chat application using React for the frontend, Node.js for the backend, and PostgreSQL for data persistence. We'll leverage WebSockets to establish a persistent connection between clients (web browsers) and the server, enabling instant message updates.
Understanding WebSockets
Imagine a two-way highway where messages flow seamlessly between clients and servers. That's the essence of WebSockets. Unlike traditional HTTP requests, which are one-off interactions, WebSockets facilitate a long-lasting connection, allowing for real-time data exchange.
WebSockets in Action
WebSockets empower a variety of applications:
- Chat Applications: Deliver messages instantly, fostering a more engaging conversation experience.
- Collaborative Editing: Enable multiple users to work on a document simultaneously, seeing changes in real-time.
- Stock Tickers: Continuously update stock prices on a financial dashboard.
- Multiplayer Games: Facilitate smooth real-time interactions among players.
- Social Media Updates: Receive notifications and updates without page reloads.
PostgreSQL: A Robust Database
PostgreSQL, a powerful open-source object-relational database (ORM), provides an excellent platform for storing chat messages and potentially user information. Its key features include:
- ACID Transactions: Ensure data integrity through atomicity, consistency, isolation, and durability.
- Scalability: Handle large chat datasets efficiently.
- Flexibility: Model complex data structures for additional features. For a convenient deployment option, consider cloud-based solutions like Rapidapp, which offers managed PostgreSQL databases, simplifying setup and maintenance.
Create a free database in Rapidapp in seconds here
Backend: Node.js WebSocket Server
Here's a breakdown of the Node.js code utilizing websocket to establish a WebSocket connection and handle message broadcasting:
const WebSocket = require('websocket').server;
const http = require('http');
const cors = require('cors');
const Sequelize = require('sequelize');
const sequelize = new Sequelize(process.env.DATABASE_URL)
const Message = sequelize.define('Message', {
username: Sequelize.DataTypes.STRING,
text: Sequelize.DataTypes.STRING,
timestamp: Sequelize.DataTypes.DATE
});
sequelize.authenticate().then(() =>{
console.log("Connection has been established successfully.")
Message.sync();
createServer();
}).catch((err) => {
console.error("Unable to connect to the database:", err)
})
function createServer() {
const httpServer = http.Server((req, res) => {
cors()(req, res, () => {
if (req.url === '/messages') {
fetchMessages().then((messages) => {
res.writeHead(200, {
'Content-Type': 'application/json'
})
res.end(JSON.stringify(messages))
})
}
})
})
const webSocketServer = new WebSocket({
httpServer: httpServer
})
webSocketServer.on('request', (req) => {
const connection = req.accept(null, req.origin);
connection.on('message', (message) => {
let msg = JSON.parse(message.utf8Data)
Message.create({
username: msg.username,
text: msg.text,
timestamp: msg.timestamp
})
webSocketServer.broadcast(JSON.stringify(message))
})
connection.on('close', () => {
// defer conn
})
})
httpServer.listen(3005, () => console.log('Listening on port 3005'));
}
function fetchMessages() {
return Message.findAll();
}
Line 1-4: Import necessary modules and libraries. We use the websocket
library to create a WebSocket server.
The http
module is used to create an HTTP server, and cors
is used to enable Cross-Origin Resource Sharing to be able to access
http backend from a different origin.
Line 6: Sequelize is a Node.js ORM library to access various databases. Create a Sequelize instance
with the connection string from Rapidapp or your own managed PostgreSQL service. The url format should be like postgresql://<user>:<password>@<host>:<port>/<dbname>?ssl=true&sslmode=no-verify&application_name=<client_name>
Line 8-12: Define a Message
model with username
, text
, and timestamp
fields.
Line 14-20: Authenticate the connection to the database and create the Message
table if it doesn't exist. If the connection is successful, start the server.
Line 24-35: Create an HTTP server to handle incoming requests. If the request URL is /messages
, fetch all messages from the database and return them as JSON.
Line 37-39: Create a WebSocket server using the websocket
library and attach it to the HTTP server.
Line 44-53: Handle incoming WebSocket requests. When a message is received, parse it, save it to the database, and broadcast it to all connected clients.
Line 60: Start the HTTP server on port 3005.
Frontend: React with WebSocket Connection
Here's the React component managing the chat interface, message sending, and receiving messages through the WebSocket connection:
import './App.css';
import {useEffect, useState, useRef} from "react";
function App() {
const [messages, setMessages] = useState([]);
const [messageInput, setMessageInput] = useState('');
const socket = useRef(null);
const [username, setUsername] = useState('');
const [existingUserName, setExistingUsername] = useState(localStorage.getItem('username') || '');
useEffect(() => {
socket.current = new WebSocket('ws://localhost:3005');
socket.current.onopen = () => {
console.log('Connected successfully.');
}
socket.current.onmessage = (event) => {
const msg = JSON.parse(event.data);
setMessages([...messages, JSON.parse(msg['utf8Data'])])
}
return () => {
socket.current.close();
};
}, [messages]);
useEffect(() => {
fetchMessages();
}, []);
const sendMessage = () => {
if (messageInput.trim() === '') {
return
}
const message = {
text: messageInput,
username: existingUserName,
timestamp: new Date().toISOString(),
}
socket.current.send(JSON.stringify(message))
setMessageInput('')
}
const fetchMessages = () => {
fetch('http://localhost:3005/messages')
.then((res) => res.json())
.then((data) => {
setMessages(data)
})
}
return (
<div className="App">
{existingUserName ? (
<div className="chat-container">
<div className="chat-messages">
{messages.map((message, index) => (
<div className="message sent">
<span className="message-timestamp">{message['username']}</span>
<span className="message-content">{message['text']}</span>
</div>
))}
</div>
<div className="chat-input">
<input
type="text"
placeholder="Type your message"
value={messageInput}
onChange={(e) => setMessageInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && messageInput.trim() !== '') {
sendMessage();
}
}}
/>
<button onClick={sendMessage}>Send</button>
</div>
</div>) : (
<div className="chat-input">
<input
type="text"
placeholder="Enter your username"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<button onClick={() => {
const u=username.trim()
localStorage.setItem('username', u);
setExistingUsername(u)
}}>Connect</button>
</div>
)}
</div>
);
}
export default App;
Line 17: Connect to the WebSocket server running on ws://localhost:3005
.
Line 23: Handle incoming messages from the WebSocket server. Parse the message and update the messages list.
Line 34: Fetch messages from the server when the component mounts.
Line 38: A function that sends a message to the WebSocket server.
Line 63: Render the chat interface. If the user has not set a username, prompt them to enter one.
Conclusion
This article has provided a foundational understanding of building a real-time chat application using React, Node.js, WebSockets, and PostgreSQL. The code snippets demonstrate the core functionalities of establishing a WebSocket connection, sending messages, broadcasting them to connected clients, and persisting messages in a database. Remember:
- This is a simplified example, and real-world applications might require additional features like user authentication, authorization, message editing/deletion, and handling disconnects gracefully.
- Consider implementing security measures to protect your application from malicious attacks.
- For large-scale applications, further optimize the database structure and explore scaling strategies.
- By building upon this foundation and tailoring it to your specific needs, you can create a dynamic and engaging real-time chat application.
You can find the complete source code for this project on GitHub.