Create a Golang App for Newsletter Management and User Authentication Using Twilio SendGrid and JWT

Nicholas DiamondNicholas Diamond
11 min read

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>&copy; 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>&copy; 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.

0
Subscribe to my newsletter

Read articles from Nicholas Diamond directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Nicholas Diamond
Nicholas Diamond