Exploring the Best Practices for Building a Golang REST API

Jennie Lee
11 min readMar 27, 2024

--

Looking for a Postman alternative?

Try APIDog, the Most Customizable Postman Alternative, where you can connect to thousands of APIs right now!

Introduction

Welcome to this tutorial on building a Golang REST API using best practices. In this article, we will explore the step-by-step process of creating a RESTful API using the Go programming language and the Gin framework.

The main goal of this project is to build an API for a personal diary app. The API will allow users to register, login, create new diary entries, and retrieve all diary entries. By following this tutorial, you will gain a solid understanding of how to build RESTful APIs using Golang and the Gin framework.

Before we begin, let’s briefly go over the prerequisites for following this tutorial. It is recommended that you have a basic understanding of Go programming and JWTs (JSON Web Tokens). Additionally, you will need the following tools installed on your machine: curl, Git, Go 1.19, and PostgreSQL with psql.

Now that we have covered the basics, let’s dive into the process of setting up the project.

Setting Up the Project

To get started, create a new folder named “diary_api” and navigate into it. Once inside the folder, initialize a new Go module by running the following command:

go mod init diary_api

This will create a new Go module with the name “diary_api”. Next, we need to install the necessary dependencies for our project. These dependencies include the Gin framework, JWT library, dotenv, cryptographic libraries, and the GORM PostgreSQL driver. You can install these dependencies by running the following command:

go get \
github.com/gin-gonic/gin \
github.com/golang-jwt/jwt/v4 \
github.com/joho/godotenv \
golang.org/x/crypto \
gorm.io/driver/postgres \
gorm.io/gorm

Once the dependencies are installed, we need to prepare the database and environment variables. Create a PostgreSQL database named “diary_app” using the following command:

createdb diary_app

Next, create a .env file in the “diary_api” folder and add the necessary database credentials to it. This file will store your local environment variables. Here is an example of what the .env file should look like:

DB_HOST=localhost
DB_PORT=5432
DB_USER=your_db_username
DB_PASSWORD=your_db_password
DB_NAME=diary_app

Additionally, create a .env.local file in the same folder with the same content, but replace the placeholder values with your actual database details.

Now that the project is set up and the database is prepared, let’s move on to creating the models.

Creating the Models

In our diary app, we will have two main models: User and Entry. We will create separate files for each model inside a “model” folder. The User model will represent a user in our system, and the Entry model will represent a diary entry.

Create a “model” folder inside the “diary_api” folder. Inside the “model” folder, create two files named “user.go” and “entry.go”.

In the “user.go” file, define the structure of the User model using Go struct tags. Here is an example of how the User model should look:

package model

import "gorm.io/gorm"

type User struct {
gorm.Model
Username string `json:"username" gorm:"unique;not null"`
Password string `json:"password,omitempty"`
}

The “gorm.Model” field provides the necessary fields (ID, CreatedAt, UpdatedAt, and DeletedAt) for database operations.

In the “entry.go” file, define the structure of the Entry model using Go struct tags. Here is an example of how the Entry model should look:

package model

import "gorm.io/gorm"

type Entry struct {
gorm.Model
UserID uint `json:"user_id" gorm:"not null"`
Title string `json:"title" gorm:"not null"`
Description string `json:"description" gorm:"not null"`
}

The “UserID” field represents the foreign key relationship to the User model.

Now that we have our models defined, let’s move on to connecting to the database.

Connecting to the Database

To connect to the PostgreSQL database, we will use the GORM PostgreSQL driver. Create a “database” folder inside the “diary_api” folder. Inside the “database” folder, create a file named “database.go”.

In the “database.go” file, we will establish a connection to the PostgreSQL database. Here is an example of how the “database.go” file should look:

package database

import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
)

func Connect() (*gorm.DB, error) {
dsn := "host=localhost user=your_db_username password=your_db_password dbname=diary_app port=5432 sslmode=disable TimeZone=Asia/Shanghai"

db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
return nil, err
}

return db, nil
}

Don’t forget to replace “your_db_username” and “your_db_password” with your actual database credentials.

Now that we have our database module in place, let’s move on to creating the entry point of our application.

Entry Point of the Application

Create a file named “main.go” at the root of the “diary_api” folder. In the “main.go” file, we will set up the entry point of our application.

First, we need to load the environment variables from the .env file. We can achieve this by using the “godotenv” package. Here is an example of how to load the environment variables:

package main

import (
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
)

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

router := gin.Default()

// Routes

router.Run(":8080")
}

Next, we need to establish a connection to the database using the “Connect()” function from the “database” package we created earlier. Here is an example of how to establish the connection:

package main

import (
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"

"diary_api/database"
)

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

db, err := database.Connect()
if err != nil {
panic("Failed to connect to the database")
}

// Gin setup

router.Run(":8080")
}

Next, we need to automatically migrate the User and Entry tables if they don’t exist. We can achieve this using the GORM’s AutoMigrate() function. Here is an example of how to automatically migrate the tables:

package main

import (
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"

"diary_api/database"
"diary_api/model"
)

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

db, err := database.Connect()
if err != nil {
panic("Failed to connect to the database")
}

err = db.AutoMigrate(&model.User{}, &model.Entry{})
if err != nil {
panic("Failed to migrate the database tables")
}

// Gin setup

router.Run(":8080")
}

Now, let’s set up the Gin router and define the registration and login routes. Here is an example of how to set up the Gin router:

package main

import (
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"

"diary_api/database"
"diary_api/model"
)

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

db, err := database.Connect()
if err != nil {
panic("Failed to connect to the database")
}

err = db.AutoMigrate(&model.User{}, &model.Entry{})
if err != nil {
panic("Failed to migrate the database tables")
}

router := gin.Default()

// Registration route

// Login route

router.Run(":8080")
}

Replace the comments with the code to define the registration and login routes. We will cover those later in the article.

Now that we have set up the entry point of our application, let’s move on to running the application.

Running the Application

To run the application, open a terminal, navigate to the root of the “diary_api” folder, and run the following command:

go run main.go

If everything is set up correctly, you should see the Gin server running on port 8080.

To verify the successful connection to the database, you can use tools like psql or a database viewer to check if the “users” and “entries” tables exist.

Congratulations! You have successfully set up the project and run the application. In the next section, we will look at setting up the authentication model.

Authentication Model Setup

In our application, we will need to implement user registration and login functionality. To achieve this, we will extend the User struct and add necessary methods for authentication.

Extend the User struct in the “user.go” file by adding the necessary fields for authentication:

package model

import (
"gorm.io/gorm"
"golang.org/x/crypto/bcrypt"
)

type User struct {
gorm.Model
Username string `json:"username" gorm:"unique;not null"`
Password string `json:"password,omitempty"`
Token string `json:"token,omitempty" gorm:"-"`
}

In addition to the existing fields, we have added a “Token” field. This field will be used to store the JWT token for authentication.

Next, let’s implement the necessary methods for user registration and login in the “controller/authentication.go” file. We will start with the user registration functionality.

Add the following Register() function to the “controller/authentication.go” file:

package controller

import (
"github.com/gin-gonic/gin"

"diary_api/database"
"diary_api/model"
)

func Register(c *gin.Context) {
var input model.User
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

db, err := database.Connect()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to connect to the database"})
return
}

hashedPassword, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash the password"})
return
}

user := model.User{
Username: input.Username,
Password: string(hashedPassword),
}

if err := db.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create the user"})
return
}

c.JSON(http.StatusOK, user)
}

The Register() function binds the JSON request from the client to the input struct, creates a connection to the database, hashes the password, and creates a new user in the database.

We also need to define the necessary routes in the “main.go” file. Add the following code snippet inside the main() function to define the registration and login routes:

router.POST("/register", controller.Register)

With these changes in place, our registration functionality is complete. In the next section, we will implement the user login functionality.

User Login

To enable login functionality, we need to add methods to the User struct for password validation and find user by username. Let’s update the “user.go” file with the following changes:

package model

import (
"gorm.io/gorm"
"golang.org/x/crypto/bcrypt"
)

type User struct {
gorm.Model
Username string `json:"username" gorm:"unique;not null"`
Password string `json:"password,omitempty"`
Token string `json:"token,omitempty" gorm:"-"`
}

func (u *User) ValidatePassword(password string) error {
return bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
}

func FindUserByUsername(username string) (*User, error) {
var user User
if err := db.Where("username = ?", username).First(&user).Error; err != nil {
return nil, err
}

return &user, nil
}

The ValidatePassword() method compares the provided password with the hashed password stored in the User struct. The FindUserByUsername() function queries the database for a user with a specific username.

Next, let’s implement the login functionality. Add the following Login() function to the “controller/authentication.go” file:

package controller

import (
"fmt"
"net/http"

"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v4"
"golang.org/x/crypto/bcrypt"

"diary_api/database"
"diary_api/model"
)

func Login(c *gin.Context) {
var input model.User
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

db, err := database.Connect()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to connect to the database"})
return
}

user, err := model.FindUserByUsername(input.Username)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
return
}

if err := user.ValidatePassword(input.Password); err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
return
}

token, err := createToken(user)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate JWT token"})
return
}

user.Token = token

c.JSON(http.StatusOK, user)
}

func createToken(user *model.User) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": user.ID,
})

return token.SignedString([]byte("your_secret_key"))
}

The Login() function binds the JSON request from the client to the input struct, creates a connection to the database, validates the credentials, and generates a JWT token using the createToken() function.

Don’t forget to add the necessary routes in the “main.go” file. Add the following code snippet inside the main() function to define the login route:

router.POST("/login", controller.Login)

With this, we have implemented the login functionality. In the next section, we will set up middleware for authenticated endpoints and add a route for creating new diary entries.

Middleware for Authenticated Endpoints and Adding Entry

To protect authenticated endpoints, we will implement JWT authentication middleware. Create a new folder called “middleware” inside the “diary_api” folder. Inside the “middleware” folder, create a file named “jwtAuth.go”.

In the “jwtAuth.go” file, add the following code:

package middleware

import (
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v4"

"diary_api/model"
)

func JWTAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
tokenString := c.GetHeader("Authorization")
if tokenString == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "No authorization token provided"})
c.Abort()
return
}

token, err := verifyToken(tokenString)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization token"})
c.Abort()
return
}

claims, ok := token.Claims.(jwt.MapClaims)
if !ok || !token.Valid {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization token"})
c.Abort()
return
}

userID, ok := claims["user_id"].(float64)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization token"})
c.Abort()
return
}

// Set the ID of the authenticated user on the request context
c.Set("user_id", uint(userID))

c.Next()
}
}

func verifyToken(tokenString string) (*jwt.Token, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Validate signing method
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}

// Return the secret key used for signing the token
return []byte("your_secret_key"), nil
})

return token, err
}

The JWTAuthMiddleware() function validates the JWT token provided in the request header and sets the authenticated user ID on the request context. The verifyToken() function handles token verification and returns a token object.

Next, let’s add a route for creating new diary entries. In the “controller/entry.go” file, add the following code:

package controller

import (
"net/http"

"github.com/gin-gonic/gin"

"diary_api/database"
"diary_api/handler"
"diary_api/middleware"
"diary_api/model"
)

func AddEntry(c *gin.Context) {
userID, _ := c.Get("user_id")
currentUserID := userID.(uint)

var input model.Entry
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

db, err := database.Connect()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to connect to the database"})
return
}

entry := model.Entry{
UserID: currentUserID,
Title: input.Title,
Description: input.Description,
}

if err := db.Create(&entry).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create the entry"})
return
}

c.JSON(http.StatusOK, entry)
}

The AddEntry() function retrieves the authenticated user ID from the request context, binds the JSON request from the client to the input struct, creates a connection to the database, and creates a new diary entry associated with the current user.

Now, let’s protect the “AddEntry” route with the authentication middleware. In the “main.go” file, add the following code to use the JWTAuthMiddleware() middleware:

router.Use(middleware.JWTAuthMiddleware())

router.POST("/entries", controller.AddEntry)

With these changes in place, we have implemented the functionality to add new diary entries. In the next section, let’s implement a feature to get all entries for an authenticated user and add protected routes.

Implementing Feature to Get All Entries for Authenticated User and Adding Protected Routes

To retrieve all entries for an authenticated user, let’s create a GetAllEntries() function in the “controller/entry.go” file. Add the following code to the file:

package controller

import (
"net/http"

"github.com/gin-gonic/gin"

"diary_api/database"
"diary_api/middleware"
"diary_api/model"
)

func GetAllEntries(c *gin.Context) {
userID, _ := c.Get("user_id")

db, err := database.Connect()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to connect to the database"})
return
}

var entries []model.Entry
if err := db.Where("user_id = ?", userID).Find(&entries).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve the entries"})
return
}

c.JSON(http.StatusOK, entries)
}

The GetAllEntries() function retrieves the authenticated user ID from the request context, creates a connection to the database, and retrieves all the diary entries associated with the current user.

Let’s now add protected routes for the GetAllEntries() function. In the “main.go” file, add the following code snippet inside the main() function:

router.GET("/entries", controller.GetAllEntries)

By adding the routes with the corresponding controller functions, we have implemented the feature to get all entries for an authenticated user.

You have now learned how to build a Golang REST API using the Gin framework. By following this tutorial, you have gained a solid understanding of building a RESTful API with Go and implementing authentication and database functionality.

Thank you for reading this tutorial. If you have any questions or feedback, feel free to join our developer-centric community and engage in discussions with other developers. Happy coding!

Looking for a Postman alternative?

Try APIDog, the Most Customizable Postman Alternative, where you can connect to thousands of APIs right now!

--

--

Jennie Lee
Jennie Lee

Written by Jennie Lee

Software Testing Blogger, #API Testing

No responses yet