Build a Customer Review App with Twilio, HTMX and Go

May 20, 2025
Written by
Temitope Taiwo Oyedele
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Build a customer review App with Twilio, HTMX and Golang

Getting customer feedback is critical for any company looking to improve its products and services. That is why having a customer review application provides an interactive way to collect valuable insights, while improving user engagement.

In this tutorial, I’ll walk you through the process of building a dynamic customer review app using Golang, HTMX, Twilio, and Gemini.

Prerequisites

Before we proceed, ensure that you have the following:

How the application works

Here’s a brief description of how the app will work. The user completes and submits a form with their name, description, and rating. Once submitted, Gemini runs a Sentiment Analysis of the feedback and provides a response based on it. Twilio then delivers an SMS notification to the product owner with the rating submission.

Get an API key from Gemini

To use Gemini for sentiment analysis, you need an API key, and there are two ways for that. The first is through Google’s AI Studio. The second is through the Google Cloud console.

Through Google AI Studio

Popup window offering options to use Google AI Studio with the Gemini API key.

To create an API through Google Cloud Console, head over to the Get API key section of Google AI Studio.

Screenshot highlighting the Create API key button in a developer console interface.

There, click on GetCreate API key. Then, click Create API key to create your API key.

Dialog box displaying a generated API key with options to copy and a reminder to keep the key secure.

After that, store the API key somewhere safe, as you’ll need it later.

Through Google Cloud Console

To create an API key using Google Cloud Console, navigate to the API credentials page, login to the Cloud Console dashboard. There, pick an active project. Then, in the navigation menu, click APIs and services > Credentials.

Google Cloud Console page showing Create credentials button highlighted.

Following that, click the Create Credentials button at the top, and in the drop-down menu, click API key:

Dropdown menu with options for creating credentials like API key, OAuth client ID, and service account.

After that, store the API key somewhere safe, as you’ll need it when you start writing codes.

Create a new Go project

Now, let's get started building the application by creating a new project directory (wherever you create your Go projects) and initialising a Go module inside it, by running the commands below.

mkdir product-review
cd product-review
go mod init

Add the environment variables

Next, you'll need the Gemini (Google) API key you created earlier, your Twilio credentials and Twilio phone number, and the phone number to which you want the message to be sent.

Create a file named .env in your project folder, then copy the configuration below into the file.

export TWILIO_ACCOUNT_SID=<<your_account_sid>>
export TWILIO_AUTH_TOKEN=<<your_auth_token>>
export TWILIO_PHONE_NUMBER=<<your_twilio_phone_number>>
export RECIPIENT_PHONE_NUMBER=<<recipient_phone_number>>
export GEMINI_API_KEY=<<your_Gemini_API_key>>

Then, replace the <<your_account_sid>> <<your_auth_token>>, and <<your_twilio_phone_number> with your Twilio Account SID, Auth Token, and phone number, respectively. You can find them in the Account Info panel of the Twilio Console dashboard.

Screenshot of Twilio account info showing Account SID, Auth Token, phone numbers, and API keys section.

Next, replace the <<recipient_phone_number>> placeholder with the phone number that you want the message sent to. Finally, replace <<your_Gemini_API_key>> with the API that you created and copied, earlier in the tutorial.

Install the required dependencies

You need to install the dependencies needed for this project, namely:

  • github.com/google/generative-ai-go/genai: This is a Go client library for interacting with Google's Generative AI API. It allows you to integrate Google's AI-powered generative models into your applications, enabling you to use it for any AI functionality you choose. In this case, it'll be used for sentiment analysis.
  • github.com/mattn/go-sqlite3: It registers the SQLite driver, which allows you to utilize SQLite as the database backend to store customer reviews.
  • github.com/twilio/twilio-go: This is the official Twilio Helper Library for Go, which simplifies interacting with Twilio services like SMS, voice, and other communication tools. For this project, it will be used to send SMS notifications to the product owner, whenever a new review is submitted.
  • google.golang.org/api/option: This package provides client options for Google APIs. It is often used to configure settings such as authentication (e.g., providing credentials via API keys or service accounts), timeouts, and other options when interacting with Google services. This project will use it to configure the Gemini API key.
  • github.com/joho/godotenv: The GoDotEnv package will help us manage our environment variables. This is optional because you might decide not to use this method, but set the environment variables in your terminal before running the Go application.

To install the dependencies, add the following:

go get github.com/google/generative-ai-go github.com/google/generative-ai-go/genai  google.golang.org/api/option github.com/mattn/go-sqlite3 github.com/twilio/twilio-go go get github.com/joho/godotenv

Let’s proceed to build the project.

Create the static and template files

The static files are the HTML and CSS files while the templates files are Go’s HTML templates. For the static templates, create a subdirectory in your root directory called static. This will contain both the HTML and CSS files. Inside this directory, create an index.html file and paste the following code:

<!DOCTYPE html>
<html lang="en">
 <head>
   <meta charset="UTF-8" />
   <meta name="viewport" content="width=device-width, initial-scale=1.0" />
   <title>Review Form</title>
   <script src="https://unpkg.com/htmx.org@1.9.2"></script>
   <link rel="stylesheet" href="/static/styles.css" />
 </head>
 <body>
   <div class="container">
     <form
       hx-post="/submit-review"
       hx-trigger="submit"
       hx-swap="outerHTML"
       hx-target="this"
     >
       <div class="form-group">
         <label for="review-name">Name:</label>
         <input type="text" id="review-name" name="review_name" required />
       </div>
       <div class="form-group">
         <label>Rating:</label>
         <div class="star-rating" id="star-rating">
           <input type="hidden" name="rating" id="rating" value="0" />
           <span class="star" data-value="1">&#9733;</span>
           <span class="star" data-value="2">&#9733;</span>
           <span class="star" data-value="3">&#9733;</span>
           <span class="star" data-value="4">&#9733;</span>
           <span class="star" data-value="5">&#9733;</span>
         </div>
       </div>
       <div class="form-group">
         <label for="review-description">Description:</label>
         <textarea
           id="review-description"
           name="review_description"
           rows="4"
           required
         ></textarea>
       </div>
       <button type="submit">Submit Review</button>
     </form>
   </div>
   <script>
     const stars = document.querySelectorAll(".star-rating .star");
     let ratingInput = document.getElementById("rating");
     let rating = 0;
     stars.forEach((star, idx) => {
       star.addEventListener("click", () => {
         rating = idx + 1;
         ratingInput.value = rating;
         resetStars();
         highlightStars(idx);
       });
       star.addEventListener("mouseover", () => {
         resetStars();
         highlightStars(idx);
       });
       star.addEventListener("mouseout", () => {
         resetStars();
         if (rating !== 0) {
           highlightStars(rating - 1);
         }
       });
     });
     function resetStars() {
       stars.forEach((star) => {
         star.classList.remove("active");
       });
     }
     function highlightStars(index) {
       for (let i = 0; i <= index; i++) {
         stars[i].classList.add("active");
       }
     }
   </script> 
 </body>
</html>

Also, create a styles.css file inside the static directory and add the following code:

body {
   font-family: Arial, sans-serif;
   display: flex;
   justify-content: center;
   align-items: center;
   height: 100vh;
   background-color: #d3d3d3;
   margin: 0;
}

.container {
   background: white;
   padding: 2rem;
   border-radius: 8px;
   box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
   border: 1px solid white;
   max-width: 400px;
   width: 100%;
}

.form-group {
   margin-bottom: 1rem;
}

label {
   display: block;
   margin-bottom: 0.5rem;
   font-weight: bold;
}

input[type="text"],
textarea {
   width: 100%;
   padding: 0.5rem;
   border: 1px solid #ccc;
   border-radius: 4px;
   box-sizing: border-box;
   font-size: 1rem;
}

button {
   background-color: #007bff;
   color: white;
   padding: 0.75rem 1.5rem;
   border: none;
   border-radius: 4px;
   cursor: pointer;
   font-size: 1rem;
   width: 100%;
   transition: background-color 0.3s;
}

button:hover {
   background-color: #0056b3;
}

.star-rating {
   display: flex;
   justify-content: center;
   font-size: 1.5rem;
   cursor: pointer;
   gap: 0.5rem;
}

.star-rating .star {
   color: #ccc;
   transition: color 0.2s;
}

.star-rating .star.active {
   color: #f5b301;
}

For the template, create a subdirectory inside the root directory of your project called templates and inside it, create a file called success.html. Add the following code:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Review Submitted</title>
    <style>
        .sentiment-message {
            margin: 20px;
            padding: 20px;
            border-radius: 5px;
            background-color: #f0f0f0;
            text-align: center;
        }
        a {
            display: inline-block;
            margin-top: 15px;
            padding: 10px 15px;
            background-color: #007bff;
            color: white;
            text-decoration: none;
            border-radius: 5px;
        }
        a:hover {
            background-color: #0056b3;
        }
    </style>
</head>
<body>
    <div class="sentiment-message">
        <p>{{.Message}}</p>
        <a href="/">Submit another review</a>
    </div>
</body>
</html>

That's all for the static files. Let’s move on to the Go part.

Build the backend

Create a file called main.go and paste in the following code:

package main

import (
    "context"
    "database/sql"
    "fmt"
    "html/template"
    "log"
    "net/http"
    "os"
    "strconv"
    "strings"
    "github.com/google/generative-ai-go/genai"
    "github.com/joho/godotenv"
    _ "github.com/mattn/go-sqlite3"
    "github.com/twilio/twilio-go"
    openapi "github.com/twilio/twilio-go/rest/api/v2010"
    "google.golang.org/api/option"
)

var templates = template.Must(template.ParseFiles("templates/success.html"))

func initDB() *sql.DB {
    database, err := sql.Open("sqlite3", "./reviews.db")
    if err != nil {
        panic(err)
    }

    createTableSQL := `CREATE TABLE IF NOT EXISTS reviews (
        "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
        "name" TEXT,
        "rating" INTEGER,
        "description" TEXT
    );`
    if _, err = database.Exec(createTableSQL); err != nil {
        panic(err)
    }
    return database
}

In the code above, the initDB() function initializes an SQLite database. It also ensures the database file, named reviews.db, exists and then creates a table for storing user reviews if it doesn't already exist. Each review contains an ID, name, rating, and description.

Initialize Gemini

Now, you need to declare a global variable that will be used to interact with the generative model. To do this, add the code below at the top of main.go — after your imported dependencies:

var geminiModel *genai.GenerativeModel

Then, add a function that initializes the Gemini AI client using the API key fetched from environment variables. Right after the database function, add the following code below:

func initGemini() {
   apiKey := os.Getenv("GEMINI_API_KEY")
   if apiKey == "" {
       log.Fatal("GEMINI_API_KEY environment variable not set. Please set it to your Gemini API key.")
   }

   ctx := context.Background()
   client, err := genai.NewClient(ctx, option.WithAPIKey(apiKey))
   if err != nil {
       log.Fatalf("Failed to create Gemini client: %v", err)
   }

   geminiModel = client.GenerativeModel("gemini-1.5-flash")
}

Create the sentiment analysis function

You next need to create a function that sends a user review description to the Gemini model for sentiment analysis. To do this, add the following code right after the initGemini() function:

func analyzeSentiment(description string) (string, error) {
    ctx := context.Background()
    prompt := fmt.Sprintf("Analyze the sentiment of the following review: %s", description)
    resp, err := geminiModel.GenerateContent(ctx, genai.Text(prompt))
    if err != nil {
        return "", err
    }

    var sentiment string
    for _, part := range resp.Candidates[0].Content.Parts {
        if textPart, ok := part.(genai.Text); ok {
            sentiment += string(textPart)
        }
    }
    return strings.TrimSpace(sentiment), nil
}

This function responds with one word: "Positive", "Negative", or "Neutral", based on the sentiment of the review.

Send an SMS with Twilio

You now need to create a function that sends an SMS via Twilio when a new review is submitted. To do this, paste the following code below the sentiment analysis function:

func sendTwilioMessage(name string, rating int) error {
    fromPhone := os.Getenv("TWILIO_PHONE_NUMBER")
    toPhone := os.Getenv("RECIPIENT_PHONE_NUMBER")
    client := twilio.NewRestClient()
    message := fmt.Sprintf("%s rated your product a %d star", name, rating)
    params := &openapi.CreateMessageParams{}
    params.SetTo(toPhone)
    params.SetFrom(fromPhone)
    params.SetBody(message)
    _, err := client.Api.CreateMessage(params)
    if err != nil {
            return fmt.Errorf("failed to send Twilio message: %w", err)
    }
    return nil
}

The message will contain the reviewer's name and the star rating, which will be sent to the product owner's phone number.

Handle form submission

Almost done. You need to create a function that handles and processes the form submission. Add the following code to your code below the sendTwilioMessage() function:

func submitReview(db *sql.DB) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        if r.Method == http.MethodPost {
            err := r.ParseForm()
            if err != nil {
                http.Error(w, "Could not parse form", http.StatusBadRequest)
                return
            }
            name := r.FormValue("review_name")
            rating, err := strconv.Atoi(r.FormValue("rating"))
            if err != nil {
                http.Error(w, "Invalid rating value", http.StatusBadRequest)
                return
            }

            description := r.FormValue("review_description")
            sentiment, err := analyzeSentiment(description)
            if err != nil {
                sentiment = "Neutral" // Default fallback
            }

            stmt, err := db.Prepare("INSERT INTO reviews (name, rating, description) VALUES (?, ?, ?)")
            if err != nil {
                http.Error(w, "Database preparation error", http.StatusInternalServerError)
                return
            }
            defer stmt.Close()

            _, err = stmt.Exec(name, rating, description)
            if err != nil {
                http.Error(w, "Failed to save review", http.StatusInternalServerError)
                return
            }

            err = sendTwilioMessage(name, rating)
            if err != nil {
                log.Printf("Failed to send Twilio message: %v", err)
            }

            var sentimentMessage string
            if sentiment == "Positive" {
                sentimentMessage = "Thank you for the positive feedback!"
            } else if sentiment == "Negative" {
                sentimentMessage = "We are sorry..."
            } else {
                sentimentMessage = "Thank you for your feedback!"
            }

            w.Header().Set("Content-Type", "text/html; charset=utf-8")
            data := struct {
                Message string
            }{
                Message: sentimentMessage,
            }
            err = templates.ExecuteTemplate(w, "success.html", data)
            if err != nil {
                log.Printf("Template execution failed: %v", err)
                http.Error(w, "Internal server error", http.StatusInternalServerError)
                return
            }
        } else {
            http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
        }
    }
}

The code above parses the form data, saves the review to the database, calls the sentiment analysis function, and sends an SMS notification. It also sends a response back to the user, including a sentiment message which is based on the customer’s opinion about the product.

Create the main function

This is the last piece of the project. You need to create the main() function. This function will:

  • Initialize the connection to the database and the Gemini API
  • Set up the HTTP server
  • Listen for incoming requests

Right after the submitReview() function code, add this:

func main() {
    err := godotenv.Load()
    if err != nil {
        log.Fatal("Error loading .env file")
    }

    db := initDB()
    defer db.Close()
    initGemini()

    fs := http.FileServer(http.Dir("./static"))
    http.Handle("/static/", http.StripPrefix("/static/", fs))
    http.Handle("/", http.FileServer(http.Dir("./static")))
    http.HandleFunc("/submit-review", submitReview(db))
    fmt.Println("Server started at :8080")

    if err := http.ListenAndServe(":8080", nil); err != nil {
        panic(err)
    }
}

And we are done!

You can access the full code on GitHub.

Test the application

To test the application, run the following command:

go run main.go

After that, navigate to http://localhost:8080/ on your browser. When the user inputs the feedback, he gets a response analyzed by Gemini based on his feedback. The product owner also gets an sms message notification about a customer rating the product.

Image showing the product review application being tested.
Smartphone screen showing a WhatsApp message

That’s how to build a product review system in Go

In the tutorial, we examined how to build a product review system using HTMX, Golang, Gemini, and Twilio. We created the form using HTMX, used Golang as the backend, used Gemini for sentiment analysis, and sent SMS using Twilio.

Communication is key to every business, and Twilio understands this. That's why it's at the forefront of providing the best platform for communication and also engaging with your customers. When you think of communication, think of Twilio.

Temitope Taiwo Oyedele is a software engineer and technical writer. He likes to write about things he’s learned and experienced.