In this tutorial, you’ll build a simple real-time chat app using Go, Fiber and HTMX.
You will learn how to leverage the versatility of Fiber by making use of a WebSocket. You’ll also learn how to create a reactive frontend without the use of JavaScript.
Prerequisites
- A good understanding of Go and HTTP servers.
- Go must be installed (Go version 1.22 will be used in this project).
Table of Contents
Getting Started
First, create a new folder named go-chat. Initiate Go in your project by running go mod init pakacage_name
, as can seen below:
go mod init github.com/steelthedev/go-chat
How to Install Dependencies
You need to install some libraries which are very vital. This can be done in the terminal by running the following commands:
go get -u github.com/gofiber/fiber/v2
go get -u github.com/gofiber/websocket/v2
go get -u github.com/gofiber/template/html/v2
These will install Fiber and other components such as WebSocket and HTML templating library.
main.go File
In the root directory, create a main.go file. This file will be the entry point to the application. Inside the file we are going to create a simple web server:
package main
import "github.com/gofiber/fiber/v2"
func main() {
// Start new fiber instance
app := fiber.New()
// Create a "ping" handler to test the server
app.Get("/ping", func(ctx *fiber.Ctx) error{
return ctx.SendString("Welcome to fiber")
})
// Start the http server
app.Listen(":3000")
}
Save the file and run go run main.go
in the terminal to start the web server.
If you head over to the browser and test the /ping
route, there should be a response like this:
Static Files
We’ll need static files such as CSS and HTML files for the application to function. Create two folders with named static and views. Inside the views folder, create two html files: index.html and messages.html.
Here’s what the index.html file should look like:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat Room</title>
<script src="https://unpkg.com/htmx.org@1.9.10"
integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
crossorigin="anonymous"></script>
<!-- HTMX Websockets extension https://htmx.org/extensions/web-sockets/ -->
<script src="https://unpkg.com/htmx.org/dist/ext/ws.js"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<link rel="stylesheet" href="http://www.freecodecamp.org/static/style.css">
</head>
<body>
<div class="container">
<div class="chat-window">
<div class="messages" id="messages" >
<!-- Messages will be appended here -->
</div>
<form id="form">
<div class="input-area">
<input type="text" name="text" min="1" id="messageInput" placeholder="Type a message...">
<button type="submit">Send</button>
</div>
</form>
</div>
</div>
</body>
</html>
In the index.html above, we have linked the necessary plugins such as our style.css which will soon be created, HTMX and bootstrap 5.
Here’s what the message.html file should look like:
<div id="messages" hx-swap-oob="beforeend">
<p class="text-small">{{ .Text }}</p>
</div>
This message will be the response from the server, it will be swapped into our index.html code automatically in the browser with the help of HTMX.
Now, create a new folder named static. Inside it, create a new file style.css:
body {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
background-color: #f2f2f2;
}
.container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.chat-window {
width: 400px;
background-color: #fff;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.messages {
padding: 10px;
overflow-y: scroll;
height: 300px;
}
.message {
margin-bottom: 10px;
}
.message p {
background-color: #f0f0f0;
border-radius: 5px;
padding: 5px 10px;
display: inline-block;
max-width: 80%;
}
.input-area {
padding: 10px;
display: flex;
}
.input-area input[type="text"] {
flex: 1;
padding: 8px;
border: 1px solid #ccc;
border-radius: 5px;
margin-right: 5px;
}
.input-area button {
padding: 8px 15px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s;
}
.input-area button:hover {
background-color: #45a049;
}
.input-area button:active {
background-color: #3e8e41;
}
How to Configure Static Files
In your main.go file, you need to tell Fiber how to handle your static files, most especially the folder to check for HTML rendering. Update main.go as follow:
package main
import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/template/html/v2"
)
func main() {
// Create views engine
viewsEngine := html.New("./views", ".html")
// Start new fiber instance
app := fiber.New(fiber.Config{
Views: viewsEngine,
})
// Static route and directory
app.Static("/static/", "./static")
// Create a "ping" handler to test the server
app.Get("/ping", func(ctx *fiber.Ctx) error{
return ctx.SendString("Welcome to fiber")
})
// Start the http server
app.Listen(":3000")
}
As seen above, a configuration was added to the app instance and also configured the static route to be /static/
.
How to Create Handlers
Create a new file named handlers.go:
package handlers
import "github.com/gofiber/fiber/v2"
type AppHandler struct{}
func NewAppHandler() *AppHandler {
return &AppHandler{}
}
func (a *AppHandler) HandleGetIndex(ctx *fiber.Ctx) error {
context := fiber.Map{}
return ctx.Render("index", context)
}
In the code above, we created a handler which received the AppHandler
struct. This helps with abstractions in case the code gets bigger. The HandleGetIndex
function takes in a pointer to the Fiber context and renders the index.html file.
In main.go:
package main
import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/template/html/v2"
"github.com/steelthedev/go-chat/handlers"
)
func main() {
// Start new fiber instance
app := fiber.New()
// Create a "ping" handler to test the server
app.Get("/ping", func(ctx *fiber.Ctx) error{
return ctx.SendString("Welcome to fiber")
})
// create new App Handler
appHandler := NewAppHandler()
// Add appHandler routes
app.Get("/, appHandler.HandleGetIndex)
// Start the http server
app.Listen(":3000")
}
Above, we created a new app handler and added the HandleGetIndex
function in the routes. Run the go run main.go
command. On localhost:3000
, you should have a screen similar to this:
messages.go File
Create a new file in the project directly and name it message.go. This file will host the message struct.
package main
type Message struct {
Text string `json:"text"`
}
websocket.go File
Create a new file in the project directory and name it websocket.go. This will house the main function creating the WebSocket server, reading through it and writing to all channels:
package main
import (
"bytes"
"encoding/json"
"html/template"
"log"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/websocket/v2"
)
type WebSocketServer struct {
clients map[*websocket.Conn]bool
broadcast chan *Message
}
func NewWebSocket() *WebSocketServer {
return &WebSocketServer{
clients: make(map[*websocket.Conn]bool),
broadcast: make(chan *Message),
}
}
func (s *WebSocketServer) HandleWebSocket(ctx *websocket.Conn) {
// Register a new Client
s.clients[ctx] = true
defer func() {
delete(s.clients, ctx)
ctx.Close()
}()
for {
_, msg, err := ctx.ReadMessage()
if err != nil {
log.Println("Read Error:", err)
break
}
// send the message to the broadcast channel
var message Message
if err := json.Unmarshal(msg, &message); err != nil {
log.Fatalf("Error Unmarshalling")
}
s.broadcast <- &message
}
}
func (s *WebSocketServer) HandleMessages() {
for {
msg := <-s.broadcast
// Send the message to all Clients
for client := range s.clients {
err := client.WriteMessage(websocket.TextMessage, getMessageTemplate(msg))
if err != nil {
log.Printf("Write Error: %v ", err)
client.Close()
delete(s.clients, client)
}
}
}
}
func getMessageTemplate(msg *Message) []byte {
tmpl, err := template.ParseFiles("views/message.html")
if err != nil {
log.Fatalf("template parsing: %s", err)
}
// Render the template with the message as data.
var renderedMessage bytes.Buffer
err = tmpl.Execute(&renderedMessage, msg)
if err != nil {
log.Fatalf("template execution: %s", err)
}
return renderedMessage.Bytes()
}
The HandleWebSocket
function adds the client, processes the messages into JSON and then adds the message into a channel for distribution to all clients by HandleMessage
.
It also keeps the connection alive. getMessageTemplate
basically process the message into the message.html, and then converts it to a byte. This byte can then be sent to the client as a response.
How to Add a WebSocket to Routes and HTMX
We need to add the WebSocket to our routes main.go:
package main
import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/template/html/v2"
"github.com/gofiber/websocket/v2"
"github.com/steelthedev/go-chat/handlers"
)
func main() {
// Start new fiber instance
app := fiber.New()
// Create a "ping" handler to test the server
app.Get("/ping", func(ctx *fiber.Ctx) error{
return ctx.SendString("Welcome to fiber")
})
// create new App Handler
appHandler := NewAppHandler()
// Add appHandler routes
app.Get("/, appHandler.HandleGetIndex)
// create new webscoket
server := NewWebSocket()
app.Get("/ws", websocket.New(func(ctx *websocket.Conn) {
server.HandleWebSocket(ctx)
}))
go server.HandleMessages()
// Start the http server
app.Listen(":3000")
}
The WebSocket and its route has been added. The final step is to add the HTMX tags on the index.html file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat Room</title>
<script src="https://unpkg.com/htmx.org@1.9.10"
integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
crossorigin="anonymous"></script>
<!-- HTMX Websockets extension https://htmx.org/extensions/web-sockets/ -->
<script src="https://unpkg.com/htmx.org/dist/ext/ws.js"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<link rel="stylesheet" href="http://www.freecodecamp.org/static/style.css">
</head>
<body>
<div class="container">
<div class="chat-window" hx-ext="ws" ws-connect="/ws">
<div class="messages" id="messages" hx-swap="beforeend" hx-swap-oob="beforeend">
<!-- Messages will be appended here -->
</div>
<form id="form" ws-send hx ">
<div class="input-area">
<input type="text" name="text" min="1" id="messageInput" placeholder="Type a message...">
<button type="submit">Send</button>
</div>
</form>
</div>
</div>
</body>
</html>
The hx-ext
tag and ws-connect
tag point to the WebSocket URL /ws
. The hx-swap
tag was used to perform DOM manipulations which adds our messages into the #messages
div.
After saving this, run go run main.go
. You can open two different browser windows at localhost:3000
If the WebSocket is running perfectly, you should be able to send and receive messages from the two browsers in real-time as displayed in the picture.
Conclusion
In this tutorial, we have showcased how to create a simple WebSocket server using Go, Fiber and HTMX.
You can go on and improve this project by adding extra features such as ClientID, authentication and user management. You can visit the project repo here: github.com/steelthedev/go-chat