Using Golang and Gin Framework to Build RESTful API - 7 Steps
Tags: go, javascript, webdev, tutorial
Published: 2023-12-12 13:01:08

More articles you can read: 
- [Using Golang to Build a Real-Time Notification System - A Step-by-Step Notification System Design Guide](https://dev.to/nikl/using-golang-to-build-a-real-time-notification-system-a-step-by-step-notification-system-design-guide-50l7)
- [JSON is Slower. Here Are Its 4 Faster Alternatives](https://dev.to/nikl/json-is-slower-here-are-its-4-faster-alternatives-2g30)

---

## Project Overview

### 1. What You Will Build

You will construct an API for a personal diary app with the following functionalities:

- Registration with a username and password
- Login with a username and password
- Creation of a new diary entry
- Retrieval of all diary entries

### 2. Prerequisites

Before diving in, ensure you have the following prerequisites:

- Basic understanding of Go and JWTs
- `curl`
- Git
- Go 1.19
- `psql`
- PostgreSQL installed on your machine

---

## Getting Started

To initiate the project, follow these steps:

1. Create a new folder named `diary_api` in your development directory and navigate into it.

```bash
mkdir diary_api
cd diary_api
```

2. Initialize a Go module with the following command.

```bash
go mod init diary_api
```

This generates a `go.mod` file to track your project's dependencies.

### 3. Install Dependencies

This project utilizes the Gin framework. Run the following command in the project's root directory to install Gin and other dependencies.

```bash
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 installed, your application gains access to essential packages such as:

- Go Cryptography for supplementary cryptographic libraries.
- GoDotEnv for managing environment variables.
- GORM, an Object Relational Mapper for Golang, along with the PostgreSQL driver for GORM to facilitate connections to PostgreSQL databases.
- JWT-Go, a Go implementation of JSON Web Tokens.

### 4. Prepare the Database and Environment Variables

Before writing any code, create a PostgreSQL database named `diary_app` with the provided `psql` command, replacing the placeholders accordingly.

```bash
createdb -h <DB_HOSTNAME> -p <DB_PORT> -U <DB_USER> diary_app --password
```

Create a new file named `.env` in the `diary_api` folder and add the following content:

```env
# Database credentials
DB_HOST="<<DB_HOST>>"
DB_USER="<<DB_USER>>"
DB_PASSWORD="<<DB_PASSWORD>>"
DB_NAME="diary_app"
DB_PORT="<<DB_PORT>>"

# Authentication credentials
TOKEN_TTL="2000"
JWT_PRIVATE_KEY="THIS_IS_NOT_SO_SECRET+YOU_SHOULD_DEFINITELY_CHANGE_IT"
```

Make a copy of `.env` named `.env.local` and replace the placeholder values with your PostgreSQL database details.

### 5. Prepare Models

Create two models, `User` and `Entry`, for the application. Start by creating a `model` folder and a file named `user.go`. Add the following code:

```go
package model

import "gorm.io/gorm"

type User struct {
    gorm.Model
    Username string `gorm:"size:255;not null;unique" json:"username"`
    Password string `gorm:"size:255;not null;" json:"-"`
    Entries  []Entry
}
```

In the `model` folder, create a file named `entry.go` and add the following code:

```go
package model

import "gorm.io/gorm"

type Entry struct {
    gorm.Model
    Content string `gorm:"type:text" json:"content"`
    UserID  uint
}
```

### 6. Connect to the Database

Create a `database` folder and a file named `database.go`. Add the following code to establish a connection to the PostgreSQL database:

```go
package database

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

var Database *gorm.DB

func Connect() {
    var err error
    host := os.Getenv("DB_HOST")
    username := os.Getenv("DB_USER")
    password := os.Getenv("DB_PASSWORD")
    databaseName := os.Getenv("DB_NAME")
    port := os.Getenv("DB_PORT")

    dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Africa/Lagos", host, username, password, databaseName, port)
    Database, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})

    if err != nil {
        panic(err)
    } else {
        fmt.Println("Successfully connected to the database")
    }
}
```

This function retrieves environment variables and opens a connection to the database using the GORM PostgreSQL driver.

### 7. Entry Point of the Application

Create a file named `main.go` at the root of the project and add the following code:

```go
package main

import (
    "diary_api/database"
    "diary_api/model"
    "github.com/joho/godotenv"
    "log"
)

func main() {
    loadEnv()
    loadDatabase()
}

func loadDatabase() {
    database.Connect()
    database.Database.AutoMigrate(&model.User{})
    database.Database.AutoMigrate(&model.Entry{})
}

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

In the `main()` function, load environment variables, establish a connection with the database, and automatically migrate tables for `User` and `Entry` structs if they don't exist.

### 8. Run the Application

Start the application by running the following command:

```bash
go run main.go
```

Upon successful startup, you should see the message: "Successfully connected to the database." Verify the presence of two new tables in your PostgreSQL database:

```sql
diary_app=# \dt
            List of relations
 Schema |  Name   | Type  | Owner
--------+---------+-------+-------
 public | entries | table | user
 public | users   | table | user
(2 rows)
```

Congratulations! You've set up the foundation for building a RESTful API using Golang and Gin. Now let's implement user registration and login in Golang-Gin RESTful API

---

## Authentication Model Setup

To begin, define the expected structure for authentication requests in the `authenticationInput.go` file within the model folder:

```go
package model

type AuthenticationInput struct {
    Username string `json:"username" binding:"required"`
    Password string `json:"password" binding:"required"`
}
```

## User Registration

Extend the `User` struct in `model/user.go` by adding the following methods:

```go
func (user *User) Save() (*User, error) {
    err := database.Database.Create(&user).Error
    if err != nil {
        return &User{}, err
    }
    return user, nil
}

func (user *User) BeforeSave(*gorm.DB) error {
    passwordHash, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
    if err != nil {
        return err
    }
    user.Password = string(passwordHash)
    user.Username = html.EscapeString(strings.TrimSpace(user.Username))
    return nil
}
```

The `Save()` function adds a new user to the database, ensuring any whitespace in the provided username is trimmed, and the password is securely hashed. Update the import statement as follows:

```go
import (
    "diary_api/database"
    "golang.org/x/crypto/bcrypt"
    "gorm.io/gorm"
    "html"
    "strings"
)
```

Create a new folder named `controller`. Inside this folder, create a new file named `authentication.go` and include the following code:

```go
package controller

import (
    "diary_api/model"
    "github.com/gin-gonic/gin"
    "net/http"
)

func Register(context *gin.Context) {
    var input model.AuthenticationInput

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

    user := model.User{
        Username: input.Username,
        Password: input.Password,
    }

    savedUser, err := user.Save()

    if err != nil {
        context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    context.JSON(http.StatusCreated, gin.H{"user": savedUser})
}
```

The `Register()` method validates the JSON request, creates a new user, and returns the saved user details as a JSON response.

## User Login

Add another method, `ValidatePassword()`, to the `User` struct for password validation. Include a `FindUserByUsername()` function in `model/user.go`:

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

func FindUserByUsername(username string) (User, error) {
    var user User
    err := database.Database.Where("username=?", username).Find(&user).Error
    if err != nil {
        return User{}, err
    }
    return user, nil
}
```

Using the bcrypt library, a hash is generated for the provided plaintext password and compared with the hash of the user’s password. An error is returned if they do not match. The `FindUserByUsername()` function queries the database to find the corresponding user.

Next, create a new folder named `helper`. In this folder, create a new file named `jwt.go` and add the following code:

```go
package helper

import (
    "diary_api/model"
    "github.com/golang-jwt/jwt/v4"
    "os"
    "strconv"
    "time"
)

var privateKey = []byte(os.Getenv("JWT_PRIVATE_KEY"))

func GenerateJWT(user model.User) (string, error) {
    tokenTTL, _ := strconv.Atoi(os.Getenv("TOKEN_TTL"))
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "id":  user.ID,
        "iat": time.Now().Unix(),
        "eat": time.Now().Add(time.Second * time.Duration(tokenTTL)).Unix(),
    })
    return token.SignedString(privateKey)
}
```

The `GenerateJWT()` function takes a user model and generates a JWT containing the user’s id (id), the time at which the token was issued (iat), and the expiry date of the token (eat). Using the JWT_PRIVATE_KEY environment variable, a signed JWT is returned as a string.

In the `controller/authentication.go` file, include the following function to handle user login:

```go
package controller

import (
	"diary_api/helper"
	"diary_api/model"
	"github.com/gin-gonic/gin"
	"net/http"
)

func Login(context *gin.Context) {
	var input model.AuthenticationInput

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

	user, err := model.FindUserByUsername(input.Username)

	if err != nil {
		context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	err = user.ValidatePassword(input.Password)

	if err != nil {
		context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	jwt, err := helper.GenerateJWT(user)
	if err != nil {
		context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	context.JSON(http.StatusOK, gin.H{"jwt": jwt})
}
```

Ensure to update the import list at the top of the file as follows:

```go
import (
	"diary_api/helper"
	"diary_api/model"
	"net/http"

	"github.com/gin-gonic/gin"
)
```

Now, update the `main.go` file to include the Gin router setup for registration and login:

```go
package main

import (
	"diary_api/controller"
	"diary_api/database"
	"diary_api/model"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/joho/godotenv"
	"log"
)

func main() {
	loadEnv()
	loadDatabase()
	serveApplication()
}

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

func loadDatabase() {
	database.Connect()
	database.Database.AutoMigrate(&model.User{})
	database.Database.AutoMigrate(&model.Entry{})
}

func serveApplication() {
	router := gin.Default()

	publicRoutes := router.Group("/auth")
	publicRoutes.POST("/register", controller.Register)
	publicRoutes.POST("/login", controller.Login)

	router.Run(":8000")
	fmt.Println("Server running on port 8000")
}
```

This modification includes the registration and login routes in the Gin router.

To test the registration and login functionality, stop the application using CTRL + c and restart it with:

```bash
go run main.go
```

Use curl to test the authentication part of the application by issuing the following commands from a new terminal session, replacing the placeholders with your username and password, respectively:

```bash
# Register
curl -i -H "Content-Type: application/json" \
    -X POST \
    -d '{"username":"your_username", "password":"your_password"}' \
    http://localhost:8000/auth/register

# Login
curl -i -H "Content-Type: application/json" \
    -X POST \
    -d '{"username":"your_username", "password":"your_password"}' \
    http://localhost:8000/auth/login
```

You should see output similar to the examples provided, returning the user details for registration and a JWT for login.

## Middleware for Authenticated Endpoints and Adding Entry

First, let's update the `helper/jwt.go` file with the provided functions:

```go
package helper

import (
	"diary_api/model"
	"errors"
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt/v4"
)

var privateKey = []byte(os.Getenv("JWT_PRIVATE_KEY"))

func ValidateJWT(context *gin.Context) error {
	token, err := getToken(context)
	if err != nil {
		return err
	}
	_, ok := token.Claims.(jwt.MapClaims)
	if ok && token.Valid {
		return nil
	}
	return errors.New("invalid token provided")
}

func CurrentUser(context *gin.Context) (model.User, error) {
	err := ValidateJWT(context)
	if err != nil {
		return model.User{}, err
	}
	token, _ := getToken(context)
	claims, _ := token.Claims.(jwt.MapClaims)
	userId := uint(claims["id"].(float64))

	user, err := model.FindUserById(userId)
	if err != nil {
		return model.User{}, err
	}
	return user, nil
}

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

		return privateKey, nil
	})
	return token, err
}

func getTokenFromRequest(context *gin.Context) string {
	bearerToken := context.Request.Header.Get("Authorization")
	splitToken := strings.Split(bearerToken, " ")
	if len(splitToken) == 2 {
		return splitToken[1]
	}
	return ""
}
```

Next, add the `FindUserById` function in the `model/user.go` file:

```go
package model

import (
    "diary_api/model"
    "errors"
    "fmt"
    "os"
    "strconv"
    "strings"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/golang-jwt/jwt/v4"
)

func FindUserById(id uint) (User, error) {
	var user User
	err := database.Database.Preload("Entries").Where("ID=?", id).Find(&user).Error
	if err != nil {
		return User{}, err
	}
	return user, nil
}
```

Now, create the `middleware/jwtAuth.go` file with the JWT authentication middleware:

```go
package middleware

import (
	"diary_api/helper"
	"github.com/gin-gonic/gin"
	"net/http"
)

func JWTAuthMiddleware() gin.HandlerFunc {
	return func(context *gin.Context) {
		err := helper.ValidateJWT(context)
		if err != nil {
			context.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
			context.Abort()
			return
		}
		context.Next()
	}
}
```

Finally, add the functionality to add a new entry in the `controller/entry.go` file:

```go
package controller

import (
	"diary_api/helper"
	"diary_api/model"
	"github.com/gin-gonic/gin"
	"net/http"
)

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

	user, err := helper.CurrentUser(context)

	if err != nil {
		context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	input.UserID = user.ID

	savedEntry, err := input.Save()

	if err != nil {
		context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	context.JSON(http.StatusCreated, gin.H{"data": savedEntry})
}
```

Ensure to import the necessary packages at the top of each file.

With these changes, you have implemented middleware for handling authenticated endpoints and added the functionality to add a new entry.

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

Add the following function to the `controller/entry.go` file:

```go
package controller

import (
	"diary_api/helper"
	"github.com/gin-gonic/gin"
	"net/http"
)

func GetAllEntries(context *gin.Context) {
	user, err := helper.CurrentUser(context)

	if err != nil {
		context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	context.JSON(http.StatusOK, gin.H{"data": user.Entries})
}
```

Update the `main.go` file to include the new protected routes:

```go
package main

import (
	"diary_api/controller"
	"diary_api/database"
	"diary_api/middleware"
	"diary_api/model"
	"fmt"
	"log"

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

func main() {
	loadEnv()
	loadDatabase()
	serveApplication()
}

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

func loadDatabase() {
	database.Connect()
	database.Database.AutoMigrate(&model.User{})
	database.Database.AutoMigrate(&model.Entry{})
}

func serveApplication() {
	router := gin.Default()

	publicRoutes := router.Group("/auth")
	publicRoutes.POST("/register", controller.Register)
	publicRoutes.POST("/login", controller.Login)

	protectedRoutes := router.Group("/api")
	protectedRoutes.Use(middleware.JWTAuthMiddleware())
	protectedRoutes.POST("/entry", controller.AddEntry)
	protectedRoutes.GET("/entry", controller.GetAllEntries)

	router.Run(":8000")
	fmt.Println("Server running on port 8000")
}
```

Now, your API has protected routes for adding and retrieving entries. Run your application using `go run main.go` and follow the instructions to create a new entry and retrieve the list of entries as described in the provided example commands.

Ensure that you replace the `<<JWT>>` placeholder with the actual JWT obtained during the login process when making requests to protected routes.

---

> #### Similar to this, I run a developer-centric community on Slack. Where we discuss these kinds of topics, implementations, integrations, some truth bombs, lunatic chats, virtual meets, and everything that will help a developer remain sane ;) Afterall, too much knowledge can be dangerous too.

> #### I'm inviting you to join our free community, take part in discussions, and share your freaking experience & expertise. You can fill out this form, and a Slack invite will ring your email in a few days. We have amazing folks from some of the great companies (Atlassian, Gong, Scaler etc), and you wouldn't wanna miss interacting with them. [Invite Form](https://forms.gle/VzA3ST8tCFrxt39U9)