Create a Golang App for Newsletter Management and User Authentication Using Twilio SendGrid and JWT
Table of contents
Introduction
In this tutorial, you'll learn how to build a Golang application that manages user authentication and sends personalized newsletters using Twilio SendGrid and JSON Web Tokens (JWT). This guide will help you streamline user management and email communication in your apps, providing a robust solution for developers.
Prerequisites
To follow this tutorial, you will need the following tools and technologies:
A free Twilio account with SendGrid enabled (signup)
Basic knowledge of Golang and web development
Go installed on your machine
Familiarity with JSON Web Tokens (JWT)
A free Render account and a PostgreSQL database
Building the app
Twilio SendGrid setup
To begin with, you'll need to sign up for a Twilio SendGrid account if you don't already have one. Visit the Twilio SendGrid website, sign up, and verify your email address.
Once verified, log in to the SendGrid Dashboard. Next, navigate to the API Keys section under Settings, where you'll create an API Key.
Click on Create API Key, choose Full Access, name your key, and then click Create & View. Make sure to copy the API key, as you will need it later in your Go application.
Setting up your project
First, you need to set up your Go project. Create a new directory for your project and initialize a Go module:
mkdir myapp
cd myapp
go mod init myapp
Installing dependencies
Before you start building the app, install the necessary Go packages for SendGrid, JWT, Gin, and other dependencies. Run the following commands in your terminal:
# Install SendGrid package
go get github.com/sendgrid/sendgrid-go
go get github.com/sendgrid/sendgrid-go/helpers/mail
# Install Gin web framework
go get github.com/gin-gonic/gin
# Install JWT package
go get github.com/golang-jwt/jwt/v5
# Install Excel handling package
go get github.com/xuri/excelize/v2
# Install Bcrypt for password hashing
go get golang.org/x/crypto/bcrypt
# Install Gorm for ORM and Postgres driver
go get gorm.io/gorm
go get gorm.io/driver/postgres
# Install package for loading environment variables
go get github.com/joho/godotenv
Setting up the database
To store user information securely, you need to set up a PostgreSQL database. In this project, you will use Gorm(docs) as the ORM to interact with your PostgreSQL database. You will spin up a PostgreSQL instance on Render and store the connection details in environment variables.
Spin up a PostgreSQL instance on Render
First, create a PostgreSQL instance on Render. Go to the Render Dashboard and sign up if you haven't already.
Once logged in, click on the New button and select PostgreSQL. Configure the PostgreSQL instance according to your needs, and then create it.
After setting up the instance, store the connection details in environment variables. In your Render Dashboard, locate and copy the connection string for your PostgreSQL instance
Next, create an .env
file in the root of your project directory. Add the following line to the .env file, replacing your_database_uri
with the connection string you copied from Render: DATABASEURI=your_database_uri
Load Environment Variables
To load environment variables from the .env
file, you will use the godotenv package. Begin by creating a file named initializers/loadEnvVariables.go
with the following content:
package initializers
import (
"log"
"github.com/joho/godotenv"
)
func LoadEnvVariables() {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
}
Connect to the Database
Next, set up the database connection using Gorm and the PostgreSQL driver. Create a file named initializers/connectToDB.go
to handle the database connection:
package initializers
import (
"os"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
var DB *gorm.DB
func ConnectToDB() {
var err error
dsn := os.Getenv("DATABASEURI")
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
panic("Failed to connect to db")
}
}
Sync the Database Schema
Finally, synchronize the database schema by creating a file name initializers/syncDB.go
:
package initializers
import "github.com/nicholas/go-jwt/models"
func SyncDatabase() {
DB.AutoMigrate(&models.User{})
}
Setting up models
To store user information in the database, you need to define the user model. Create a file named models/userModel.go
with the following content:
package models
import "gorm.io/gorm"
type User struct {
gorm.Model
Name string
Email string `gorm:"unique"`
Password string
}
Implementing email sending with SendGrid
Next, implement email sending functionality using Twilio SendGrid. Create a file named controllers/email.go
and add the following code:
package controllers
import (
"bytes"
"fmt"
"html/template"
"os"
"github.com/sendgrid/sendgrid-go"
"github.com/sendgrid/sendgrid-go/helpers/mail"
)
func SendEmail(toEmail, toName, subject, templatePath string, data interface{}) error {
tmpl, err := template.ParseFiles(templatePath)
if err != nil {
return err
}
var body bytes.Buffer
err = tmpl.Execute(&body, data)
if err != nil {
return err
}
from := mail.NewEmail("Your App Name", os.Getenv("SENDGRID_FROM"))
to := mail.NewEmail(toName, toEmail)
plainTextContent := "This is a plain text version of the email."
htmlContent := body.String()
message := mail.NewSingleEmail(from, subject, to, plainTextContent, htmlContent)
client := sendgrid.NewSendClient(os.Getenv("SENDGRID_API_KEY"))
response, err := client.Send(message)
if err != nil {
return err
} else if response.StatusCode >= 400 {
return fmt.Errorf("failed to send email: %s", response.Body)
}
return nil
}
This function sends an email using a specified template. Be sure to replace "Your App Name"
with the actual name of your application.
Setting up user authentication
To handle user registration and authentication, create a file named controllers/auth.go
:
package controllers
import (
"fmt"
"net/http"
"os"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/xuri/excelize/v2"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"github.com/nicholas/go-jwt/initializers"
"github.com/nicholas/go-jwt/models"
)
func Signup(c *gin.Context) {
var body struct {
Name string
Email string
Password string
}
if c.Bind(&body) != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read body"})
return
}
hash, err := bcrypt.GenerateFromPassword([]byte(body.Password), 10)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to hash password"})
return
}
user := models.User{Name: body.Name, Email: body.Email, Password: string(hash)}
result := initializers.DB.Create(&user)
if result.Error != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to create user"})
return
}
err = SendEmail(user.Email, user.Name, "Welcome!", "templates/welcome.html", struct{ Name string }{Name: user.Name})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to send welcome email"})
return
}
c.JSON(http.StatusOK, gin.H{})
}
func Login(c *gin.Context) {
var body struct {
Email string
Password string
}
if c.Bind(&body) != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read body"})
return
}
var user models.User
initializers.DB.First(&user, "email = ?", body.Email)
if user.ID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid Email"})
return
}
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(body.Password))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid Password"})
return
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": user.ID,
"exp": time.Now().Add(time.Hour * 24 * 30).Unix(),
})
tokenString, err := token.SignedString([]byte(os.Getenv("JWTSECRET")))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Unable to create token"})
return
}
c.JSON(http.StatusOK, gin.H{"token": tokenString})
}
func Validate(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "I'm logged in"})
}
Next, create a file named templates/welcome.html
for the user registration email template:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Welcome to Our App</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f9f9f9;
margin: 0;
padding: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
background: #fff;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h1 {
color: #0056b3;
}
p {
margin-bottom: 20px;
}
.button {
display: inline-block;
padding: 10px 20px;
margin-top: 20px;
background-color: #0056b3;
color: #fff;
text-decoration: none;
border-radius: 5px;
}
.footer {
margin-top: 20px;
font-size: 0.9em;
text-align: center;
color: #777;
}
</style>
</head>
<body>
<div class="container">
<h1>Hello {{.Name}}, Welcome to Our App!</h1>
<p>
We're thrilled to have you on board. Get ready to explore our services
and discover everything we have to offer.
</p>
<p>To help you get started, here are a few tips:</p>
<ul>
<li>
<strong>Explore Features:</strong> Dive into our app and explore the
wide range of features designed to enhance your experience.
</li>
<li>
<strong>Stay Updated:</strong> Keep an eye on your inbox for updates,
tips, and exclusive offers.
</li>
<li>
<strong>Connect with Us:</strong> Join our community on social media
and share your experiences.
</li>
</ul>
<p>
We're here to support you every step of the way. If you have any
questions or need assistance, don't hesitate to reach out to our support
team.
</p>
<a href="https://example.com" class="button">Get Started</a>
</div>
<div class="footer">
<p>© 2024 Our App. All rights reserved.</p>
<p><a href="#">Unsubscribe</a> | <a href="#">Contact Us</a></p>
</div>
</body>
</html>
Finally, navigate back to the controllers/auth.go
file and add the necessary function to allow registered users to log in successfully:
package controllers
import (
"fmt"
"net/http"
"os"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/xuri/excelize/v2"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"github.com/nicholas/go-jwt/initializers"
"github.com/nicholas/go-jwt/models"
)
func Signup(c *gin.Context) {
var body struct {
Name string
Email string
Password string
}
if c.Bind(&body) != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read body"})
return
}
hash, err := bcrypt.GenerateFromPassword([]byte(body.Password), 10)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to hash password"})
return
}
user := models.User{Name: body.Name, Email: body.Email, Password: string(hash)}
result := initializers.DB.Create(&user)
if result.Error != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to create user"})
return
}
err = SendEmail(user.Email, user.Name, "Welcome!", "templates/welcome.html", struct{ Name string }{Name: user.Name})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to send welcome email"})
return
}
c.JSON(http.StatusOK, gin.H{})
}
func Login(c *gin.Context) {
var body struct {
Email string
Password string
}
if c.Bind(&body) != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read body"})
return
}
var user models.User
initializers.DB.First(&user, "email = ?", body.Email)
if user.ID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid Email"})
return
}
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(body.Password))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid Password"})
return
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": user.ID,
"exp": time.Now().Add(time.Hour * 24 * 30).Unix(),
})
tokenString, err := token.SignedString([]byte(os.Getenv("JWTSECRET")))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Unable to create token"})
return
}
c.JSON(http.StatusOK, gin.H{"token": tokenString})
}
func Validate(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "I'm logged in"})
}
Handling newsletter sending
To send newsletters, you need to create a function in controllers/newsletter.go
. This function will manage the process of sending newsletters to users:
package controllers
import (
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/xuri/excelize/v2"
)
func UploadAndSendEmails(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to upload file"})
return
}
f, err := file.Open()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to open file"})
return
}
defer f.Close()
xlFile, err := excelize.OpenReader(f)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read Excel file"})
return
}
rows, err := xlFile.GetRows("Sheet1")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get rows"})
return
}
fmt.Println("Email list:", rows)
var emails []string
for _, row := range rows {
if len(row) > 0 {
emails = append(emails, row[0])
}
}
for i := 0; i < len(emails); i += 100 {
end := i + 100
if end > len(emails) {
end = len(emails)
}
batch := emails[i:end]
for _, email := range batch {
err := SendEmail(email, "Subscriber", "Newsletter", "templates/newsletter.html", nil)
if err != nil {
fmt.Println("Failed to send email to:", email, "Error:", err)
}
}
time.Sleep(1 * time.Minute)
}
c.JSON(http.StatusOK, gin.H{"message": "Emails sent successfully"})
}
Next, you need to create an email template for the newsletters. To do this, create a file named templates/newsletter.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Go Newsletter</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f9f9f9;
margin: 0;
padding: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
background: #fff;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h1 {
color: #0056b3;
}
p {
margin-bottom: 20px;
}
.button {
display: inline-block;
padding: 10px 20px;
margin-top: 20px;
background-color: #0056b3;
color: #fff;
text-decoration: none;
border-radius: 5px;
}
.footer {
margin-top: 20px;
font-size: 0.9em;
text-align: center;
color: #777;
}
</style>
</head>
<body>
<div class="container">
<h1>Welcome to Go Newsletter!</h1>
<p>Hello Subscriber,</p>
<p>
We are thrilled to have you on board. In this newsletter, you'll find
the latest updates, exclusive insights, and exciting news about our app.
Our goal is to keep you informed and engaged with all the happenings in
our community.
</p>
<p>Stay tuned for:</p>
<ul>
<li>Latest features and updates</li>
<li>Tips and tricks for getting the most out of our app</li>
<li>Exclusive offers and promotions</li>
<li>Upcoming events and webinars</li>
</ul>
<p>
We're committed to providing you with valuable content that helps you
get the best experience possible. If you have any feedback or
suggestions, feel free to reply to this email. We'd love to hear from
you!
</p>
<a href="https://example.com" class="button">Visit Our Website</a>
</div>
<div class="footer">
<p>© 2024 Go Newsletter. All rights reserved.</p>
<p><a href="#">Unsubscribe</a> | <a href="#">Contact Us</a></p>
</div>
</body>
</html>
Setting up the main application
Finally, configure the main application by setting up the main.go
file. This is where you will integrate all the components and ensure the application runs smoothly:
package main
import (
"github.com/gin-gonic/gin"
"github.com/nicholas/go-jwt/controllers"
"github.com/nicholas/go-jwt/initializers"
"github.com/nicholas/go-jwt/middleware"
)
func init() {
initializers.LoadEnvVariables()
initializers.ConnectToDB()
initializers.SyncDatabase()
}
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "pong"})
})
r.POST("/signup", controllers.Signup)
r.POST("/login", controllers.Login)
r.POST("/upload", controllers.UploadAndSendEmails)
r.Run()
}
Testing, troubleshooting, or product demonstration
To test your application, start by running it using the command go run main.go
.
Once the application is running, use a tool like Postman or curl to test the /signup
, /login
, and /upload
endpoints to ensure they are functioning correctly.
Additionally, check your SendGrid account to verify that emails are being sent as expected.
Next, create an excel file containing a list of email addresses for sending out newsletters.
Make a POST request with this Excel file and verify that each email address receives the newsletter emails.
If you encounter issues, verify the following:
Your SendGrid API key is correctly set in your environment variables.
The JWT secret (JWTSECRET) is properly configured.
The database and templates are correctly set up.
Conclusion
In this tutorial, you've built a Go application that integrates with Twilio SendGrid for sending newsletters and uses JWT for user authentication. You can now expand this application by adding more features or integrating with other services. For further learning, consider exploring Twilio's documentation and Go's official site.
Nicholas Diamond is a software engineer with experience in building web applications and integrating third-party services.
Subscribe to my newsletter
Read articles from Nicholas Diamond directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by