Building a Real-Time Chat Application with Go and React
Real-time communication is at the heart of many modern web applications, from collaborative tools to social platforms. A chat application is a quintessential example of real-time functionality. This guide will walk you through building a full-stack real-time chat application using Go for the backend and React for the frontend, powered by WebSockets for instant messaging.
1. Project Overview and Architecture
Our chat application will feature:
- User authentication (registration and login)
- Real-time messaging between users
- Persistent chat history
- Online user presence
- A responsive and intuitive user interface
1.1. Technology Stack
Backend (Go):
- Gin Web Framework: For building the REST API and serving static files.
- Gorilla WebSocket: For handling WebSocket connections.
- GORM: As the ORM for database interactions.
- SQLite: As the database for simplicity (can be replaced with PostgreSQL/MySQL).
- JWT: For stateless user authentication.
Frontend (React):
- React: For building the user interface.
- React Router: For client-side routing.
- Axios: For making HTTP requests to the backend API.
- Tailwind CSS: For styling and responsive design.
1.2. Architecture Diagram
┌─────────────────┐ ┌────────────────────┐
│ React Frontend│◄───────►│ Go Backend API │
│ (WebSocket) │ │ (Gin + Gorilla) │
└─────────────────┘ └────────────────────┘
│
▼
┌────────────────────┐
│ Database │
│ (SQLite) │
└────────────────────┘
2. Setting Up the Backend
2.1. Project Initialization
Create the project directory and initialize the Go module:
mkdir go-react-chat
cd go-react-chat
go mod init go-react-chat
2.2. Installing Dependencies
# Web framework and utilities
go get -u github.com/gin-gonic/gin
go get -u github.com/gorilla/websocket
go get -u github.com/golang-jwt/jwt/v5
go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite
go get -u golang.org/x/crypto/bcrypt
2.3. Project Structure
go-react-chat/
├── go.mod
├── go.sum
├── main.go
├── internal/
│ ├── auth/
│ │ ├── auth.go
│ │ └── middleware.go
│ ├── chat/
│ │ ├── hub.go
│ │ ├── client.go
│ │ └── message.go
│ ├── database/
│ │ └── database.go
│ ├── models/
│ │ ├── user.go
│ │ └── message.go
│ └── routes/
│ └── routes.go
└── client/ # This will be our React frontend
2.4. Database Models
Let's start by defining our data models.
// internal/models/user.go
package models
import (
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
Username string `gorm:"uniqueIndex;not null" json:"username"`
Email string `gorm:"uniqueIndex;not null" json:"email"`
Password string `gorm:"not null" json:"-"` // Don't serialize password
}
// BeforeCreate hashes the user's password before saving to the database
func (u *User) BeforeCreate(tx *gorm.DB) error {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
if err != nil {
return err
}
u.Password = string(hashedPassword)
return nil
}
// CheckPassword compares a plain text password with the hashed password
func (u *User) CheckPassword(password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
return err == nil
}
// internal/models/message.go
package models
import "time"
type Message struct {
ID uint `gorm:"primaryKey" json:"id"`
Content string `gorm:"not null" json:"content"`
UserID uint `json:"user_id"`
User User `json:"user"` // Preload user information
CreatedAt time.Time `json:"created_at"`
}
2.5. Database Connection
// internal/database/database.go
package database
import (
"fmt"
"log"
"go-react-chat/internal/models"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
var DB *gorm.DB
func Connect() {
var err error
DB, err = gorm.Open(sqlite.Open("chat.db"), &gorm.Config{})
if err != nil {
log.Fatal("Failed to connect to the database:", err)
}
// Auto Migrate the schema
err = DB.AutoMigrate(&models.User{}, &models.Message{})
if err != nil {
log.Fatal("Failed to migrate database:", err)
}
fmt.Println("Database connected and migrated!")
}
2.6. Authentication System
// internal/auth/auth.go
package auth
import (
"errors"
"time"
"os"
"go-react-chat/internal/database"
"go-react-chat/internal/models"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
)
var jwtKey = []byte(os.Getenv("JWT_SECRET"))
if len(jwtKey) == 0 {
jwtKey = []byte("my_secret_key") // Fallback for development
}
type Claims struct {
UserID uint `json:"user_id"`
jwt.RegisteredClaims
}
// Signup creates a new user
func Signup(username, email, password string) (*models.User, error) {
// Check if user already exists
var existingUser models.User
if err := database.DB.Where("username = ? OR email = ?", username, email).First(&existingUser).Error; err == nil {
return nil, errors.New("user already exists with this username or email")
}
user := models.User{
Username: username,
Email: email,
Password: password, // This will be hashed by the BeforeCreate hook
}
if err := database.DB.Create(&user).Error; err != nil {
return nil, err
}
return &user, nil
}
// Login authenticates a user and returns a JWT token
func Login(username, password string) (string, error) {
var user models.User
if err := database.DB.Where("username = ?", username).First(&user).Error; err != nil {
return "", errors.New("invalid credentials")
}
if !user.CheckPassword(password) {
return "", errors.New("invalid credentials")
}
// Create JWT token
expirationTime := time.Now().Add(24 * time.Hour)
claims := &Claims{
UserID: user.ID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(jwtKey)
if err != nil {
return "", err
}
return tokenString, nil
}
// GetUserFromToken retrieves the user ID from a JWT token
func GetUserFromToken(tokenString string) (*Claims, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
return jwtKey, nil
})
if err != nil {
return nil, err
}
if !token.Valid {
return nil, errors.New("invalid token")
}
return claims, nil
}
// internal/auth/middleware.go
package auth
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
// AuthMiddleware checks for a valid JWT token in the Authorization header
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"})
c.Abort()
return
}
// Expected format: "Bearer <token>"
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
if tokenString == authHeader {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Bearer token required"})
c.Abort()
return
}
claims, err := GetUserFromToken(tokenString)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
c.Abort()
return
}
// Set user ID in context for use in handlers
c.Set("userID", claims.UserID)
c.Next()
}
}
2.7. WebSocket Chat Hub
The chat hub is the core of our real-time functionality, managing all connected clients and broadcasting messages.
// internal/chat/hub.go
package chat
import (
"log"
"go-react-chat/internal/database"
"go-react-chat/internal/models"
)
// Hub maintains the set of active clients and broadcasts messages.
type Hub struct {
// Registered clients.
clients map[*Client]bool
// Inbound messages from the clients.
broadcast chan *Message
// Register requests from the clients.
register chan *Client
// Unregister requests from clients.
unregister chan *Client
}
func NewHub() *Hub {
return &Hub{
broadcast: make(chan *Message),
register: make(chan *Client),
unregister: make(chan *Client),
clients: make(map[*Client]bool),
}
}
func (h *Hub) Run() {
for {
select {
case client := <-h.register:
h.clients[client] = true
// Send list of online users to the new client
h.sendOnlineUsers(client)
case client := <-h.unregister:
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
// Notify other clients that this user is offline
h.broadcastUserStatus(client, false)
}
case message := <-h.broadcast:
// Save message to database
dbMessage := models.Message{
Content: message.Content,
UserID: message.UserID,
}
database.DB.Create(&dbMessage)
// Load user information for the message
var user models.User
database.DB.First(&user, dbMessage.UserID)
dbMessage.User = user
// Broadcast message to all clients
for client := range h.clients {
select {
case client.send <- &Message{
Type: "chat",
Content: dbMessage.Content,
Username: dbMessage.User.Username,
UserID: dbMessage.UserID,
Timestamp: dbMessage.CreatedAt,
}:
default:
close(client.send)
delete(h.clients, client)
}
}
}
}
}
// sendOnlineUsers sends a list of online users to a specific client
func (h *Hub) sendOnlineUsers(client *Client) {
var onlineUsers []string
for c := range h.clients {
onlineUsers = append(onlineUsers, c.username)
}
select {
case client.send <- &Message{Type: "online_users", Users: onlineUsers}:
default:
// Handle failed send if necessary
}
}
// broadcastUserStatus notifies all clients about a user's online/offline status
func (h *Hub) broadcastUserStatus(client *Client, isOnline bool) {
status := "offline"
if isOnline {
status = "online"
}
message := &Message{
Type: "user_status",
Username: client.username,
Status: status,
}
for c := range h.clients {
select {
case c.send <- message:
default:
// Handle failed send
}
}
}
// internal/chat/client.go
package chat
import (
"bytes"
"log"
"net/http"
"time"
"github.com/gorilla/websocket"
"go-react-chat/internal/database"
"go-react-chat/internal/models"
)
const (
// Time allowed to write a message to the peer.
writeWait = 10 * time.Second
// Time allowed to read the next pong message from the peer.
pongWait = 60 * time.Second
// Send pings to peer with this period. Must be less than pongWait.
pingPeriod = (pongWait * 9) / 10
// Maximum message size allowed from peer.
maxMessageSize = 512
)
var (
newline = []byte{'\n'}
space = []byte{' '}
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
// Allow connections from any origin in development
// In production, restrict this to known origins
return true
},
}
// Client is a middleman between the websocket connection and the hub.
type Client struct {
hub *Hub
// The websocket connection.
conn *websocket.Conn
// Buffered channel of outbound messages.
send chan *Message
// User information
userID uint
username string
}
// ServeWs handles websocket requests from the peer.
func ServeWs(hub *Hub, w http.ResponseWriter, r *http.Request, userID uint) {
// Get user information
var user models.User
if err := database.DB.First(&user, userID).Error; err != nil {
http.Error(w, "User not found", http.StatusUnauthorized)
return
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
client := &Client{
hub: hub,
conn: conn,
send: make(chan *Message, 256),
userID: userID,
username: user.Username,
}
client.hub.register <- client
// Allow collection of memory referenced by the caller by doing all work in
// new goroutines.
go client.writePump()
go client.readPump()
}
// readPump pumps messages from the websocket connection to the hub.
func (c *Client) readPump() {
defer func() {
c.hub.unregister <- c
c.conn.Close()
}()
c.conn.SetReadLimit(maxMessageSize)
c.conn.SetReadDeadline(time.Now().Add(pongWait))
c.conn.SetPongHandler(func(string) error {
c.conn.SetReadDeadline(time.Now().Add(pongWait));
return nil
})
for {
_, message, err := c.conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Printf("error: %v", err)
}
break
}
message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1))
// Create and broadcast the message
msg := &Message{
Content: string(message),
UserID: c.userID,
}
c.hub.broadcast <- msg
}
}
// writePump pumps messages from the hub to the websocket connection.
func (c *Client) writePump() {
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
c.conn.Close()
}()
for {
select {
case message, ok := <-c.send:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if !ok {
// The hub closed the channel.
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
w, err := c.conn.NextWriter(websocket.TextMessage)
if err != nil {
return
}
// Serialize the message to JSON
jsonMessage, err := message.ToJSON()
if err != nil {
log.Printf("Error serializing message: %v", err)
continue
}
w.Write(jsonMessage)
// Add queued chat messages to the current websocket message.
n := len(c.send)
for i := 0; i < n; i++ {
w.Write(newline)
jsonMessage, err := (<-c.send).ToJSON()
if err != nil {
log.Printf("Error serializing queued message: %v", err)
continue
}
w.Write(jsonMessage)
}
if err := w.Close(); err != nil {
return
}
case <-ticker.C:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}
// internal/chat/message.go
package chat
import (
"encoding/json"
"time"
)
type Message struct {
Type string `json:"type"` // "chat", "user_status", "online_users"
Content string `json:"content,omitempty"`
Username string `json:"username,omitempty"`
UserID uint `json:"user_id,omitempty"`
Timestamp time.Time `json:"timestamp,omitempty"`
Status string `json:"status,omitempty"` // "online", "offline"
Users []string `json:"users,omitempty"` // For online_users message
}
func (m *Message) ToJSON() ([]byte, error) {
return json.Marshal(m)
}
2.8. API Routes
// internal/routes/routes.go
package routes
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"go-react-chat/internal/auth"
"go-react-chat/internal/chat"
"go-react-chat/internal/database"
"go-react-chat/internal/models"
)
func SetupRoutes(router *gin.Engine, hub *chat.Hub) {
// Public routes
public := router.Group("/api")
{
public.POST("/signup", signup)
public.POST("/login", login)
}
// Protected routes
protected := router.Group("/api")
protected.Use(auth.AuthMiddleware())
{
protected.GET("/ws", func(c *gin.Context) {
userID, _ := c.Get("userID")
chat.ServeWs(hub, c.Writer, c.Request, userID.(uint))
})
protected.GET("/messages", getMessages)
protected.GET("/users", getUsers)
}
}
type SignupRequest struct {
Username string `json:"username" binding:"required"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
}
func signup(c *gin.Context) {
var req SignupRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user, err := auth.Signup(req.Username, req.Email, req.Password)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"user": user})
}
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
func login(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
token, err := auth.Login(req.Username, req.Password)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"token": token})
}
func getMessages(c *gin.Context) {
var messages []models.Message
database.DB.Preload("User").Order("created_at asc").Find(&messages)
c.JSON(http.StatusOK, gin.H{"messages": messages})
}
func getUsers(c *gin.Context) {
var users []models.User
database.DB.Select("id, username").Find(&users)
c.JSON(http.StatusOK, gin.H{"users": users})
}
2.9. Main Application Entry Point
// main.go
package main
import (
"log"
"os"
"github.com/gin-gonic/gin"
"go-react-chat/internal/chat"
"go-react-chat/internal/database"
"go-react-chat/internal/routes"
)
func main() {
// Set Gin to release mode in production
if os.Getenv("GIN_MODE") != "debug" {
gin.SetMode(gin.ReleaseMode)
}
// Connect to the database
database.Connect()
// Create the chat hub
hub := chat.NewHub()
go hub.Run() // Run the hub in a separate goroutine
// Create Gin router
router := gin.Default()
// Serve React frontend (to be built later)
router.Static("/", "./client/build")
// Setup API routes
routes.SetupRoutes(router, hub)
// Get port from environment variable or default to 8080
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
log.Printf("Server starting on port %s", port)
log.Fatal(router.Run(":" + port))
}
3. Setting Up the Frontend with React
3.1. Creating the React Application
We'll create the React application inside the client
directory.
npx create-react-app client
cd client
npm install axios react-router-dom
3.2. Project Structure
client/
├── public/
├── src/
│ ├── components/
│ │ ├── Chat.jsx
│ │ ├── Login.jsx
│ │ ├── Register.jsx
│ │ └── Navbar.jsx
│ ├── context/
│ │ └── AuthContext.jsx
│ ├── hooks/
│ │ └── useChat.js
│ ├── services/
│ │ └── api.js
│ ├── App.js
│ ├── App.css
│ └── index.js
├── package.json
└── ...
3.3. Authentication Context
// client/src/context/AuthContext.jsx
import React, { createContext, useState, useEffect } from 'react';
const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [auth, setAuth] = useState({
token: localStorage.getItem('token'),
isAuthenticated: null,
loading: true,
user: null
});
useEffect(() => {
if (auth.token) {
setAuth(prev => ({ ...prev, isAuthenticated: true, loading: false }));
} else {
setAuth(prev => ({ ...prev, isAuthenticated: false, loading: false }));
}
}, [auth.token]);
const login = (token) => {
localStorage.setItem('token', token);
setAuth({
token,
isAuthenticated: true,
loading: false,
user: null
});
};
const logout = () => {
localStorage.removeItem('token');
setAuth({
token: null,
isAuthenticated: false,
loading: false,
user: null
});
};
return (
<AuthContext.Provider value={{ auth, login, logout }}>
{children}
</AuthContext.Provider>
);
};
export default AuthContext;
3.4. API Service
// client/src/services/api.js
import axios from 'axios';
const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || 'http://localhost:8080/api';
// Create an axios instance
const api = axios.create({
baseURL: API_BASE_URL
});
// Add a request interceptor to include the auth token
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Add a response interceptor to handle auth errors
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response && error.response.status === 401) {
// Token is invalid, remove it and redirect to login
localStorage.removeItem('token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default api;
3.5. Authentication Components
// client/src/components/Register.jsx
import React, { useState, useContext } from 'react';
import { useNavigate } from 'react-router-dom';
import AuthContext from '../context/AuthContext';
import api from '../services/api';
const Register = () => {
const [formData, setFormData] = useState({
username: '',
email: '',
password: ''
});
const [error, setError] = useState('');
const { login } = useContext(AuthContext);
const navigate = useNavigate();
const { username, email, password } = formData;
const onChange = e => setFormData({ ...formData, [e.target.name]: e.target.value });
const onSubmit = async e => {
e.preventDefault();
try {
const res = await api.post('/signup', formData);
console.log('Registration successful', res.data);
navigate('/login');
} catch (err) {
console.error('Registration error:', err);
if (err.response && err.response.data && err.response.data.error) {
setError(err.response.data.error);
} else {
setError('Registration failed. Please try again.');
}
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
Create a new account
</h2>
</div>
<form className="mt-8 space-y-6" onSubmit={onSubmit}>
{error && <div className="text-red-500 text-center">{error}</div>}
<input type="hidden" name="remember" value="true" />
<div className="rounded-md shadow-sm -space-y-px">
<div>
<input
id="username"
name="username"
type="text"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Username"
value={username}
onChange={onChange}
/>
</div>
<div>
<input
id="email-address"
name="email"
type="email"
autoComplete="email"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Email address"
value={email}
onChange={onChange}
/>
</div>
<div>
<input
id="password"
name="password"
type="password"
autoComplete="new-password"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Password"
value={password}
onChange={onChange}
/>
</div>
</div>
<div>
<button
type="submit"
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Sign up
</button>
</div>
</form>
</div>
</div>
);
};
export default Register;
// client/src/components/Login.jsx
import React, { useState, useContext } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import AuthContext from '../context/AuthContext';
import api from '../services/api';
const Login = () => {
const [formData, setFormData] = useState({
username: '',
password: ''
});
const [error, setError] = useState('');
const { login } = useContext(AuthContext);
const navigate = useNavigate();
const { username, password } = formData;
const onChange = e => setFormData({ ...formData, [e.target.name]: e.target.value });
const onSubmit = async e => {
e.preventDefault();
try {
const res = await api.post('/login', formData);
login(res.data.token);
navigate('/chat');
} catch (err) {
console.error('Login error:', err);
if (err.response && err.response.data && err.response.data.error) {
setError(err.response.data.error);
} else {
setError('Login failed. Please check your credentials.');
}
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
Sign in to your account
</h2>
</div>
<form className="mt-8 space-y-6" onSubmit={onSubmit}>
{error && <div className="text-red-500 text-center">{error}</div>}
<input type="hidden" name="remember" value="true" />
<div className="rounded-md shadow-sm -space-y-px">
<div>
<input
id="username"
name="username"
type="text"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Username"
value={username}
onChange={onChange}
/>
</div>
<div>
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="Password"
value={password}
onChange={onChange}
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="text-sm">
<Link to="/register" className="font-medium text-indigo-600 hover:text-indigo-500">
Don't have an account? Sign up
</Link>
</div>
</div>
<div>
<button
type="submit"
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Sign in
</button>
</div>
</form>
</div>
</div>
);
};
export default Login;
3.6. Chat Component with WebSocket
// client/src/hooks/useChat.js
import { useState, useEffect, useRef } from 'react';
const useChat = (token) => {
const [messages, setMessages] = useState([]);
const [users, setUsers] = useState([]);
const [inputMessage, setInputMessage] = useState('');
const ws = useRef(null);
useEffect(() => {
if (!token) return;
// Create WebSocket connection
const wsUrl = process.env.REACT_APP_WS_URL || `ws://localhost:8080/api/ws`;
ws.current = new WebSocket(`${wsUrl}?token=${token}`);
ws.current.onopen = () => {
console.log('Connected to WebSocket');
};
ws.current.onmessage = (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'chat':
setMessages(prev => [...prev, {
id: Date.now(), // Simple ID for frontend
content: data.content,
username: data.username,
timestamp: data.timestamp
}]);
break;
case 'online_users':
setUsers(data.users);
break;
case 'user_status':
if (data.status === 'online') {
setUsers(prev => [...new Set([...prev, data.username])]);
} else {
setUsers(prev => prev.filter(user => user !== data.username));
}
break;
default:
console.log('Unknown message type:', data.type);
}
};
ws.current.onclose = () => {
console.log('Disconnected from WebSocket');
};
// Fetch initial messages
fetchInitialMessages();
// Cleanup function
return () => {
if (ws.current) {
ws.current.close();
}
};
}, [token]);
const fetchInitialMessages = async () => {
try {
const response = await fetch('/api/messages', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const data = await response.json();
setMessages(data.messages);
}
} catch (error) {
console.error('Error fetching initial messages:', error);
}
};
const sendMessage = () => {
if (inputMessage.trim() && ws.current && ws.current.readyState === WebSocket.OPEN) {
ws.current.send(inputMessage);
setInputMessage('');
}
};
return {
messages,
users,
inputMessage,
setInputMessage,
sendMessage
};
};
export default useChat;
// client/src/components/Chat.jsx
import React, { useContext } from 'react';
import { useNavigate } from 'react-router-dom';
import AuthContext from '../context/AuthContext';
import useChat from '../hooks/useChat';
const Chat = () => {
const { auth, logout } = useContext(AuthContext);
const navigate = useNavigate();
const token = localStorage.getItem('token');
const {
messages,
users,
inputMessage,
setInputMessage,
sendMessage
} = useChat(token);
const handleLogout = () => {
logout();
navigate('/login');
};
const handleKeyPress = (e) => {
if (e.key === 'Enter') {
sendMessage();
}
};
return (
<div className="flex h-screen bg-gray-100">
{/* Sidebar */}
<div className="w-64 bg-white border-r border-gray-300 flex flex-col">
<div className="p-4 border-b border-gray-300">
<h1 className="text-xl font-bold">Chat App</h1>
</div>
<div className="flex-1 overflow-y-auto">
<div className="p-4">
<h2 className="text-lg font-semibold mb-2">Online Users</h2>
<ul>
{users.map((user, index) => (
<li key={index} className="py-2 px-4 hover:bg-gray-100 rounded">
{user}
</li>
))}
</ul>
</div>
</div>
<div className="p-4 border-t border-gray-300">
<button
onClick={handleLogout}
className="w-full py-2 px-4 bg-red-500 hover:bg-red-600 text-white rounded"
>
Logout
</button>
</div>
</div>
{/* Main Chat Area */}
<div className="flex-1 flex flex-col">
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4">
<div className="space-y-4">
{messages.map((msg) => (
<div key={msg.id} className="bg-white p-4 rounded shadow">
<div className="flex justify-between items-center mb-2">
<span className="font-semibold text-indigo-600">{msg.username}</span>
<span className="text-xs text-gray-500">
{new Date(msg.timestamp).toLocaleTimeString()}
</span>
</div>
<p>{msg.content}</p>
</div>
))}
</div>
</div>
{/* Input */}
<div className="p-4 border-t border-gray-300 bg-white">
<div className="flex">
<input
type="text"
className="flex-1 border border-gray-300 rounded-l py-2 px-4 focus:outline-none"
placeholder="Type a message..."
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
onKeyPress={handleKeyPress}
/>
<button
className="bg-indigo-600 hover:bg-indigo-700 text-white py-2 px-4 rounded-r"
onClick={sendMessage}
>
Send
</button>
</div>
</div>
</div>
</div>
);
};
export default Chat;
3.7. Main Application Component
// client/src/App.js
import React, { useContext } from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import AuthContext from './context/AuthContext';
import Login from './components/Login';
import Register from './components/Register';
import Chat from './components/Chat';
const App = () => {
const { auth } = useContext(AuthContext);
// Show loading spinner while checking auth status
if (auth.loading) {
return <div className="min-h-screen flex items-center justify-center">Loading...</div>;
}
return (
<Router>
<Routes>
<Route
path="/"
element={auth.isAuthenticated ? <Navigate to="/chat" /> : <Navigate to="/login" />}
/>
<Route
path="/login"
element={!auth.isAuthenticated ? <Login /> : <Navigate to="/chat" />}
/>
<Route
path="/register"
element={!auth.isAuthenticated ? <Register /> : <Navigate to="/chat" />}
/>
<Route
path="/chat"
element={auth.isAuthenticated ? <Chat /> : <Navigate to="/login" />}
/>
</Routes>
</Router>
);
};
export default App;
3.8. Update Index.js
// client/src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { AuthProvider } from './context/AuthContext';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<AuthProvider>
<App />
</AuthProvider>
</React.StrictMode>
);
4. Running the Application
4.1. Backend
In the root project directory (go-react-chat
):
- Make sure you have SQLite installed.
- Run the Go application:bashThis will start the backend server on
go run main.go
http://localhost:8080
.
4.2. Frontend
In the client
directory:
- Install dependencies (if not already done):bash
npm install
- Start the React development server:bashThis will start the frontend on
npm start
http://localhost:3000
and automatically open it in your browser.
5. Deployment Considerations
5.1. Building for Production
For the frontend, create a production build:
cd client
npm run build
This will create a build
folder with optimized static files.
For the backend, you can build the Go binary:
cd ..
go build -o chat-app
5.2. Environment Variables
Set the following environment variables for production:
GIN_MODE=release
PORT=8080
(or your desired port)JWT_SECRET=your_super_secret_key
5.3. Database
For production, consider using a more robust database like PostgreSQL or MySQL instead of SQLite, and set up proper connection pooling.
5.4. Reverse Proxy
Use a reverse proxy like Nginx to serve the React frontend and proxy API requests to the Go backend. This also helps with SSL termination.
6. Conclusion
This guide has walked you through building a full-stack real-time chat application with Go and React. We've covered:
- Backend Development: Creating a REST API with Gin, implementing user authentication with JWT, and building a WebSocket-based real-time chat system.
- Frontend Development: Building a React application with user authentication, real-time messaging via WebSockets, and a responsive UI with Tailwind CSS.
- Database Integration: Using GORM with SQLite for data persistence.
- Deployment: Considerations for deploying the application to a production environment.
This application provides a solid foundation that you can extend with features like:
- Private messaging
- Chat rooms or channels
- Message reactions
- File sharing
- Push notifications
- End-to-end encryption
By understanding the principles and patterns demonstrated in this project, you can build more complex and feature-rich real-time applications.