Skip to main content

Building a Realtime Chat App with React, Node.js, and PostgreSQL

· 6 min read
Huseyin BABAL
Software Developer

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.
tip

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:

server.js
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:

App.js
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.
tip

You can find the complete source code for this project on GitHub.