REST API Implementation with Go

Last updated Nov 03, 2021

In this go language tutorial series the REST API development with Go is explained. REST API is an application that provides many resources that can be used by other application such as mobile application. In this article, the REST API is created to manage products data.

Project Setup

Create a project by this command. Choose domain name based on your repository.

go mod init github.com/nadirbasalamah/go-products-api

There are many dependencies that added in this project:

  1. fiber to develop REST API in Go.
  2. uuid to create an ID.
  3. validator to perform validation.
  4. godotenv to read configuration from .env file.
  5. gorm to use ORM feature to communicate with database.
  6. mysql to use MySQL database.
  7. jwt to authentication method using JSON Web Token (JWT).
  8. bcrypt to encrypt a password.
go get github.com/gofiber/fiber/v2
go get github.com/google/uuid
go get github.com/go-playground/validator/v10
go get github.com/joho/godotenv
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
go get -u github.com/gofiber/jwt/v3
go get -u github.com/golang-jwt/jwt/v4
go get -u golang.org/x/crypto/bcrypt

After all dependencies are installed, create an .env file to store configuration for database and authentication using JWT.

DB_HOST=localhost
DB_PORT=3306
DB_USER=YOUR_DB_USERNAME
DB_PASSWORD=YOUR_DB_PASSWORD
DB_NAME=YOUR_DB_NAME
JWT_SECRET_KEY=YOUR_SECRET_KEY
JWT_SECRET_KEY_EXPIRE_MINUTES_COUNT=YOUR_MINUTES_COUNT

 

Create a new directory called config then create a new file config.go to read value from .env file.

package config

import (
    "log"
    "os"

    "github.com/joho/godotenv"
)

// Config return value from .env file
func Config(key string) string {
    err := godotenv.Load(".env")
    if err != nil {
        log.Fatalf("Error loading .env file\n")
    }
    return os.Getenv(key)
}

 

Creating Models

Create a new directory called model to store created models.

A model that represents product entity is created in product.go.

package model

// Product represents product entity
type Product struct {
    ID          string
    Name        string
    Description string
    Price       float64
    Stock       int
}

A model that represents validation error message is created in errorResponse.go.

package model

type ErrorResponse struct {
    FailedField string
    Tag         string
    Value       string
}

A model that represents product data input is created in productInput.go.

package model

import "github.com/go-playground/validator/v10"

// ProductInput represents input for product data
type ProductInput struct {
    Name        string  `validate:"required"`
    Description string  `validate:"required"`
    Price       float64 `validate:"required"`
    Stock       int     `validate:"required"`
}

// ValidateStruct is used to validate product input
func (productInput ProductInput) ValidateStruct() []*ErrorResponse {
    var errors []*ErrorResponse
    validate := validator.New()
    err := validate.Struct(productInput)
    if err != nil {
        for _, err := range err.(validator.ValidationErrors) {
            var element ErrorResponse
            element.FailedField = err.StructNamespace()
            element.Tag = err.Tag()
            errors = append(errors, &element)
        }
    }

    return errors
}

 

A model that represents user entity is created in user.go.

package model

// User represents user entity
type User struct {
    ID       string
    Email    string `gorm:"unique"`
    Password string
}

A model that represents data input for user is created in userInput.go.

package model

import "github.com/go-playground/validator/v10"

type UserInput struct {
    Email    string `validate:"required,email"`
    Password string `validate:"required"`
}

// ValidateStruct is used to validate input for user data
func (userInput UserInput) ValidateStruct() []*ErrorResponse {
    var errors []*ErrorResponse
    validate := validator.New()
    err := validate.Struct(userInput)
    if err != nil {
        for _, err := range err.(validator.ValidationErrors) {
            var element ErrorResponse
            element.FailedField = err.StructNamespace()
            element.Tag = err.Tag()
            errors = append(errors, &element)
        }
    }

    return errors
}

 

Make sure the MySQL server is started and database that will be connected is created. Create a new directory called database then create a new file called database.go to connect and perform database migration for users and products table.

package database

import (
    "fmt"

    "github.com/nadirbasalamah/go-products-api/config"
    "github.com/nadirbasalamah/go-products-api/model"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

var DB *gorm.DB

func InitDatabase() {
    var dataSource string = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", config.Config("DB_USER"), config.Config("DB_PASSWORD"), config.Config("DB_HOST"), config.Config("DB_PORT"), config.Config("DB_NAME"))
    var err error

    DB, err = gorm.Open(mysql.Open(dataSource), &gorm.Config{})

    if err != nil {
        panic(err.Error())
    }

    fmt.Println("Connected!")

    DB.AutoMigrate(&model.Product{})

    DB.AutoMigrate(&model.User{})
}

 

Creating Repository

Repository is a component to interact with database. To create a repository component, create a new directory called repository.

A repository component to manage product data is created in repository.go.

package repository

import (
    "github.com/google/uuid"
    "github.com/nadirbasalamah/go-products-api/database"
    "github.com/nadirbasalamah/go-products-api/model"
)

func GetAllProducts() []model.Product {
    var products []model.Product

    database.DB.Find(&products)

    return products
}

func GetProductById(id string) (model.Product, int64) {
    var product model.Product

    result := database.DB.First(&product, "id = ?", id)

    if result.RowsAffected == 0 {
        return model.Product{}, 0
    }

    return product, result.RowsAffected
}

func CreateProduct(input model.ProductInput) model.Product {
    var product model.Product = model.Product{
        ID:          uuid.New().String(),
        Name:        input.Name,
        Description: input.Description,
        Price:       input.Price,
        Stock:       input.Stock,
    }

    database.DB.Create(&product)

    return product
}

func UpdateProduct(id string, input model.ProductInput) model.Product {
    product, rows := GetProductById(id)

    if rows != 0 {
        product.Name = input.Name
        product.Description = input.Description
        product.Price = input.Price
        product.Stock = input.Stock

        database.DB.Save(&product)

        return product
    }

    return model.Product{}
}

func DeleteProduct(id string) bool {
    product, rows := GetProductById(id)

    if rows != 0 {
        database.DB.Delete(&product)
        return true
    }

    return false
}

 

A repository component to perform authentication is created in auth.go.

package repository

import (
    "errors"

    "github.com/google/uuid"
    "github.com/nadirbasalamah/go-products-api/database"
    "github.com/nadirbasalamah/go-products-api/model"
    "github.com/nadirbasalamah/go-products-api/utils"
    "golang.org/x/crypto/bcrypt"
)

func Register(userInput model.UserInput) (string, error) {
    password, err := bcrypt.GenerateFromPassword([]byte(userInput.Password), bcrypt.DefaultCost)

    if err != nil {
        return "", err
    }

    var user model.User = model.User{
        ID:       uuid.New().String(),
        Email:    userInput.Email,
        Password: string(password),
    }

    database.DB.Create(&user)

    token, err := utils.GenerateNewAccessToken()

    if err != nil {
        return "", err
    }

    return token, nil
}

func Login(userInput model.UserInput) (string, error) {
    var user model.User

    result := database.DB.First(&user, "email = ?", userInput.Email)

    if result.RowsAffected == 0 {
        return "", errors.New("User not found")
    }

    err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(userInput.Password))

    if err != nil {
        return "", errors.New("Invalid password")
    }

    token, err := utils.GenerateNewAccessToken()

    if err != nil {
        return "", err
    }

    return token, nil
}

 

Creating Service

Service is a component that acts as a "bridge" between controller and repository. To create a service component, create a new directory called service.

A service component to manage product data is created in service.go.

package service

import (
    "github.com/nadirbasalamah/go-products-api/model"
    "github.com/nadirbasalamah/go-products-api/repository"
)

func GetAllProducts() []model.Product {
    return repository.GetAllProducts()
}

func GetProductById(id string) (model.Product, int64) {
    product, rows := repository.GetProductById(id)
    return product, rows
}

func CreateProduct(input model.ProductInput) model.Product {
    return repository.CreateProduct(input)
}

func UpdateProduct(id string, input model.ProductInput) model.Product {
    return repository.UpdateProduct(id, input)
}

func DeleteProduct(id string) bool {
    return repository.DeleteProduct(id)
}

A service component to perform authentication is created in auth.go.

package service

import (
    "github.com/nadirbasalamah/go-products-api/model"
    "github.com/nadirbasalamah/go-products-api/repository"
)

func Register(userInput model.UserInput) (string, error) {
    return repository.Register(userInput)
}

func Login(userInput model.UserInput) (string, error) {
    return repository.Login(userInput)
}

 

Creating Controllers

Controller is a component to perform request. to create a controller component, create a new directory called controller.

A controller to manage product data is created in controller.go.

package controller

import (
    "github.com/gofiber/fiber/v2"
    "github.com/nadirbasalamah/go-products-api/model"
    "github.com/nadirbasalamah/go-products-api/service"
    "github.com/nadirbasalamah/go-products-api/utils"
)

func GetAllProducts(c *fiber.Ctx) error {
    var products []model.Product = service.GetAllProducts()

    return c.JSON(products)
}

func GetProductById(c *fiber.Ctx) error {
    var productId string = c.Params("id")

    product, rows := service.GetProductById(productId)

    if rows == 0 {
        return c.Status(404).JSON(fiber.Map{
            "message": "Data not found",
        })
    }

    return c.JSON(product)
}

func CreateProduct(c *fiber.Ctx) error {
    isValid, err := utils.CheckToken(c)

    if !isValid {
        return c.Status(400).JSON(fiber.Map{"message": err.Error()})
    }

    var productInput *model.ProductInput = new(model.ProductInput)

    if err := c.BodyParser(productInput); err != nil {
        c.Status(503).SendString(err.Error())
        return err
    }

    errors := productInput.ValidateStruct()

    if errors != nil {
        return c.Status(400).JSON(errors)
    }

    var createdProduct model.Product = service.CreateProduct(*productInput)

    return c.JSON(createdProduct)
}

func UpdateProduct(c *fiber.Ctx) error {
    isValid, err := utils.CheckToken(c)

    if !isValid {
        return c.Status(400).JSON(fiber.Map{"message": err.Error()})
    }

    var productInput *model.ProductInput = new(model.ProductInput)

    if err := c.BodyParser(productInput); err != nil {
        c.Status(503).SendString(err.Error())
        return err
    }

    errors := productInput.ValidateStruct()

    if errors != nil {
        return c.Status(400).JSON(errors)
    }

    var productId string = c.Params("id")

    var updatedProduct model.Product = service.UpdateProduct(productId, *productInput)

    if updatedProduct.ID == "" {
        return c.Status(404).JSON(fiber.Map{"message": "Data not found"})
    }

    return c.JSON(updatedProduct)
}

func DeleteProduct(c *fiber.Ctx) error {
    isValid, err := utils.CheckToken(c)

    if !isValid {
        return c.Status(400).JSON(fiber.Map{"message": err.Error()})
    }

    var productId string = c.Params("id")

    var result bool = service.DeleteProduct(productId)

    if result {
        return c.JSON(fiber.Map{"message": "Data deleted"})
    }

    return c.Status(404).JSON(fiber.Map{"message": "Data not found"})
}

A controller to perform authentication is created in auth.go.

package controller

import (
    "github.com/gofiber/fiber/v2"
    "github.com/nadirbasalamah/go-products-api/model"
    "github.com/nadirbasalamah/go-products-api/service"
)

func Register(c *fiber.Ctx) error {
    var userInput *model.UserInput = new(model.UserInput)

    if err := c.BodyParser(userInput); err != nil {
        c.Status(503).SendString(err.Error())
        return err
    }

    errors := userInput.ValidateStruct()

    if errors != nil {
        return c.Status(400).JSON(errors)
    }

    token, err := service.Register(*userInput)

    if err != nil {
        c.Status(500).JSON(err)
        return err
    }

    return c.JSON(fiber.Map{"token": token})
}

func Login(c *fiber.Ctx) error {
    var userInput *model.UserInput = new(model.UserInput)

    if err := c.BodyParser(userInput); err != nil {
        c.Status(503).SendString(err.Error())
        return err
    }

    errors := userInput.ValidateStruct()

    if errors != nil {
        return c.Status(400).JSON(errors)
    }

    token, err := service.Login(*userInput)

    if err != nil {
        c.Status(500).JSON(err)
        return err
    }

    return c.JSON(fiber.Map{"token": token})
}

 

Creating Middleware for Authentication

Middleware can be used to check if the request can be executed or not. The middleware component is created in middleware directory inside middleware.go file.

package middleware

import (
    "github.com/gofiber/fiber/v2"
    jwtMiddleware "github.com/gofiber/jwt/v3"
    "github.com/nadirbasalamah/go-products-api/config"
)

func JWTProtected() func(*fiber.Ctx) error {
    config := jwtMiddleware.Config{
        SigningKey:   []byte(config.Config("JWT_SECRET_KEY")),
        ContextKey:   "jwt",
        ErrorHandler: jwtError,
    }

    return jwtMiddleware.New(config)

}

func jwtError(c *fiber.Ctx, err error) error {
    if err.Error() == "Missing or malformed JWT" {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "message": err.Error(),
        })
    }

    return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
        "message": err.Error(),
    })
}

 

Creating Utilities for Authentication

Some utility functions are created for authentication. The utility is created in utils.go file that located inside utils directory.

package utils

import (
    "strconv"
    "strings"
    "time"

    "github.com/gofiber/fiber/v2"
    "github.com/golang-jwt/jwt/v4"
    "github.com/nadirbasalamah/go-products-api/config"
)

type TokenMetadata struct {
    Expires int64
}

func GenerateNewAccessToken() (string, error) {
    secret := config.Config("JWT_SECRET_KEY")

    minutesCount, _ := strconv.Atoi(config.Config("JWT_SECRET_KEY_EXPIRE_MINUTES_COUNT"))

    claims := jwt.MapClaims{}

    claims["exp"] = time.Now().Add(time.Minute * time.Duration(minutesCount)).Unix()

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

    t, err := token.SignedString([]byte(secret))

    if err != nil {
        return "", err
    }

    return t, nil
}

func ExtractTokenMetadata(c *fiber.Ctx) (*TokenMetadata, error) {
    token, err := verifyToken(c)

    if err != nil {
        return nil, err
    }

    claims, ok := token.Claims.(jwt.MapClaims)

    if ok && token.Valid {
        expires := int64(claims["exp"].(float64))

        return &TokenMetadata{
            Expires: expires,
        }, nil
    }

    return nil, err
}

func CheckToken(c *fiber.Ctx) (bool, error) {
    now := time.Now().Unix()

    claims, err := ExtractTokenMetadata(c)
    if err != nil {
        return false, err
    }

    expires := claims.Expires

    if now > expires {
        return false, err
    }

    return true, nil
}

func extractToken(c *fiber.Ctx) string {
    bearToken := c.Get("Authorization")

    onlyToken := strings.Split(bearToken, " ")
    if len(onlyToken) == 2 {
        return onlyToken[1]
    }

    return ""
}

func verifyToken(c *fiber.Ctx) (*jwt.Token, error) {
    tokenString := extractToken(c)

    token, err := jwt.Parse(tokenString, jwtKeyFunc)
    if err != nil {
        return nil, err
    }

    return token, nil
}

func jwtKeyFunc(token *jwt.Token) (interface{}, error) {
    return []byte(config.Config("JWT_SECRET_KEY")), nil
}

 

Creating Routes

The route is created to define a certain URL for certain request. The public route is a request that can be executed without authentication. The private route is a request for authenticated user. The route is created in route.go file inside route directory.

package route

import (
    "github.com/gofiber/fiber/v2"
    "github.com/nadirbasalamah/go-products-api/controller"
    "github.com/nadirbasalamah/go-products-api/middleware"
)

// public routes
func SetupRoutes(app *fiber.App) {
    app.Get("/api/products", controller.GetAllProducts)
    app.Get("/api/products/:id", controller.GetProductById)

    app.Post("/api/register", controller.Register)
    app.Post("/api/login", controller.Login)
}

// private routes
func SetupPrivateRoutes(app *fiber.App) {
    app.Post("/api/products", middleware.JWTProtected(), controller.CreateProduct)
    app.Put("/api/products/:id", middleware.JWTProtected(), controller.UpdateProduct)
    app.Delete("/api/products/:id", middleware.JWTProtected(), controller.DeleteProduct)
}

 

Final Setup

Create a file called server.go then the route is configured in this file. The database is also configured.

package main

import (
    "github.com/gofiber/fiber/v2"
    "github.com/nadirbasalamah/go-products-api/database"
    "github.com/nadirbasalamah/go-products-api/route"
)

func main() {
    app := fiber.New()

    database.InitDatabase()

    route.SetupRoutes(app)

    route.SetupPrivateRoutes(app)

    app.Listen(":3000")
}

 

Start the server with go run server.go command. Make sure the MySQL server is started.

The REST API can be tested using Postman or using other tools like cURL. The register can be performed through http://localhost:3000/api/register with POST method.

{
  "email": "aiden@jackson.com",
  "password": "123123"
}

Output.

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzU0OTk1ODB9.7LplMwbv1FtkSHlFTrkCB1BabxBkC45jo2wtghg5Gjs"
}

The token can be used for create, edit and delete operation for product data. The token can be added inside Authorization menu (choose Bearer Token for authentication type) if the Postman is used.

In this example, the create operation is executed in http://localhost:3000/api/products with POST method.

{
  "name": "data science quick guide",
  "description": "great book about data science",
  "price": 80,
  "stock": 100
}

Output.

{
  "ID": "112581f4-0bf3-43b1-b94f-d2ae2a7d115b",
  "Name": "data science quick guide",
  "Description": "great book about data science",
  "Price": 80,
  "Stock": 100
}

 

The REST API implementation repository in this article can be checked here.

I hope this article is helpful to learn REST API development with Go programming language.

Article Contributed By :
https://www.rrtutors.com/site_assets/profile/assets/img/avataaars.svg

1179 Views