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.
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:
fiber
to develop REST API in Go.uuid
to create an ID.validator
to perform validation.godotenv
to read configuration from .env
file.gorm
to use ORM feature to communicate with database.mysql
to use MySQL database.jwt
to authentication method using JSON Web Token (JWT).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) } |
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{}) } |
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 } |
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) } |
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}) } |
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(), }) } |
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 } |
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) } |
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 :
|
|
|
|
1406 Views |