In this tutorial, you’ll learn how to build a Golang CRUD API example using the Fiber framework and GORM to interact with a MySQL database.

Typically, I tend to use Gin Gonic when building APIs in Golang, but after exploring Fiber, I must say that it is much better. It offers straightforward APIs and an architecture that is reminiscent of the Express framework in Node.js. So, if you’re already familiar with working with Express, then you’ll feel right at home when working with Fiber as the transfer of knowledge is seamless.

GORM provides a clean and elegant way to perform CRUD operations on a SQL database, making it a great choice for our project. However, it’s important to note that it may not be the most performant option for handling high traffic volumes. In those cases, SQLC may be a better choice. Nonetheless, for this tutorial, we’ll be using GORM.

Without further ado, let’s dive right into the tutorial to construct the Golang CRUD API using Fiber, GORM, and MySQL.

More practice:

Golang CRUD API Example with GORM, Fiber, and MySQL

Run the CRUD API Project on Your Machine

If you want to run the Golang CRUD API project on your computer, you can follow these steps:

  1. Firstly, download or clone the Golang Fiber CRUD project from https://github.com/wpcodevo/golang-fiber-mysql and open the source code in a code editor of your choice.
  2. Next, open the integrated terminal in your IDE or text editor and start the MySQL server in Docker by running the command docker-compose up -d.
  3. After that, run the command go run main.go in the terminal to install the necessary packages, migrate the GORM schema to the MySQL database, and start the Fiber HTTP server.
  4. To test the CRUD endpoints, you can import the Note App.postman_collection.json file into Postman and make HTTP requests to the Fiber server. Alternatively, if you want to set up a frontend application to interact with the API, you can follow the instructions below.

Run the CRUD API with a Frontend App

If you want a more detailed walkthrough on building a React.js CRUD app, you can check out the post titled “Build a React.js CRUD App using a RESTful API“. However, the steps below should be enough to get you started without having to write any code.

  1. Before you start, make sure you have Node.js and Yarn installed on your machine. If not, you can download and install them from the official websites.
  2. Next, get the source code for the React CRUD project by either cloning the repository or downloading it from https://github.com/wpcodevo/reactjs-crud-note-app, and open it in your favorite IDE or text editor.
  3. In the console, navigate to the root directory of the project and run the command yarn or yarn install to install all of the necessary dependencies.
  4. Once the dependencies have been installed, run the command yarn dev to start the Vite development server.
  5. To test the CRUD functionalities, open the app in your browser at http://localhost:3000/. Keep in mind that accessing the React app at http://127.0.0.1:3000 may result in a site can’t be reached or CORS errors. Therefore, it’s best to use http://localhost:3000/.

Bootstrap the Golang Project

Upon completing this tutorial, you’ll have a file and folder organization that mirrors the screenshot displayed below, except for the Makefile and Note App.postman_collection.json.

Folder Structure of the Golang, Fiber, MySQL and GORM Project

To start building the project, navigate to a preferred directory on your computer and open a terminal there. Once in the directory, run the following commands to create a new project folder, switch to the new folder, and initialize a new Golang project. Be sure to replace wpcodevo with your GitHub username.


mkdir golang-fiber-mysql
cd golang-fiber-mysql
go mod init github.com/wpcodevo/golang-fiber-mysql

Now that you have initialized the Golang project, it’s time to open it in your preferred code editor. Once you have done that, you can launch the integrated terminal of your IDE and install the necessary packages for the project by running the following commands.


go get github.com/gofiber/fiber/v2
go get github.com/google/uuid
go get github.com/go-playground/validator/v10
go get -u gorm.io/gorm
go get gorm.io/driver/mysql
go get github.com/spf13/viper
go install github.com/cosmtrek/air@latest

  • fiber – A high-performance web framework for Golang that supports middleware, routing, and error handling, as well as many other features commonly found in web frameworks.
  • uuid – A library to generate and parse UUIDs in Golang.
  • validator – A library for validating Go structs, values, and variables.
  • gorm – Provides a rich set of features such as automatic database schema generation, association handling, and query building, making it a popular choice for building web applications and services in Go.
  • mysql – A MySQL driver for GORM
  • viper – A library for managing configuration files in Golang.
  • air – This library automatically rebuilds and restarts your application.

Before we start implementing the CRUD functionality, let’s first create a simple Fiber server that responds with a JSON object when a GET request is made to the health checker route. To do this, create a file named main.go in the root directory and add the following code to it.

main.go


package main

import (
	"log"

	"github.com/gofiber/fiber/v2"
)

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

	app.Get("/api/healthchecker", func(c *fiber.Ctx) error {
		return c.Status(200).JSON(fiber.Map{
			"status":  "success",
			"message": "Welcome to Golang, Fiber, and GORM",
		})
	})

	log.Fatal(app.Listen(":8000"))
}

You now have the choice to either build and run the project using the standard Go command-line tool or to use the air tool. If you choose to use air, it will automatically monitor your source code for any changes and reload the server accordingly. This means that you won’t have to manually stop and restart the server each time you make changes to the code.

After starting the server, you can check that it’s running correctly by visiting http://localhost:8000/api/healthchecker in your web browser. This should display a JSON object.

Testing the Health Checker Route of the Golang, MySQL, Fiber, and GORM API

Setup MySQL Server with Docker

To set up the MySQL database for this project, we’ll launch a MySQL server inside a Docker container. This approach offers greater flexibility and control than downloading the MySQL binary from the official website.

To get started, create a new file called docker-compose.yml in the root directory and add the following Docker Compose configurations.

docker-compose.yml


version: '3'
services:
  mysql:
    image: mysql:latest
    container_name: mysql
    env_file:
      - ./app.env
    ports:
      - '6500:3306'
    volumes:
      - mysqlDB:/var/lib/mysql
volumes:
  mysqlDB:

If you take a closer look at the Docker Compose configuration, you will notice that the MySQL credentials are being read from an app.env file, which has not been created yet. To create this file, navigate to the root directory of the project and add the following environment variables to the app.env file.

app.env


MYSQL_HOST=127.0.0.1
MYSQL_PORT=6500
MYSQL_DATABASE=golang_gorm
MYSQL_USER=admin
MYSQL_PASSWORD=password123
MYSQL_ROOT_PASSWORD=password123

DATABASE_URL=mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@localhost:${MYSQL_PORT}/${MYSQL_DATABASE}

CLIENT_ORIGIN=http://localhost:3000

Create the Database Model with GORM

Now that the MySQL server is up and running, it’s time to define the database model using GORM. Start by creating a folder named ‘models‘ in the root directory of your project. Inside the ‘models‘ folder, create a file called note.model.go and add the following code. For the sake of simplicity, we’ll only create one model for this tutorial.

models/note.model.go


package models

import (
	"time"

	"github.com/go-playground/validator/v10"
	"github.com/google/uuid"
	"gorm.io/gorm"
)

type Note struct {
	ID        string    `gorm:"type:char(36);primary_key" json:"id,omitempty"`
	Title     string    `gorm:"type:varchar(255);uniqueIndex:idx_notes_title,LENGTH(255);not null" json:"title,omitempty"`
	Content   string    `gorm:"not null" json:"content,omitempty"`
	Category  string    `gorm:"varchar(100)" json:"category,omitempty"`
	Published bool      `gorm:"default:false;not null" json:"published"`
	CreatedAt time.Time `gorm:"not null;default:'1970-01-01 00:00:01'" json:"createdAt,omitempty"`
	UpdatedAt time.Time `gorm:"not null;default:'1970-01-01 00:00:01';ON UPDATE CURRENT_TIMESTAMP" json:"updatedAt,omitempty"`
}

func (note *Note) BeforeCreate(tx *gorm.DB) (err error) {
	note.ID = uuid.New().String()
	return nil
}

var validate = validator.New()

type ErrorResponse struct {
	Field string `json:"field"`
	Tag   string `json:"tag"`
	Value string `json:"value,omitempty"`
}

func ValidateStruct[T any](payload T) []*ErrorResponse {
	var errors []*ErrorResponse
	err := validate.Struct(payload)
	if err != nil {
		for _, err := range err.(validator.ValidationErrors) {
			var element ErrorResponse
			element.Field = err.StructNamespace()
			element.Tag = err.Tag()
			element.Value = err.Param()
			errors = append(errors, &element)
		}
	}
	return errors
}

type CreateNoteSchema struct {
	Title     string `json:"title" validate:"required"`
	Content   string `json:"content" validate:"required"`
	Category  string `json:"category,omitempty"`
	Published bool   `json:"published,omitempty"`
}

type UpdateNoteSchema struct {
	Title     string `json:"title,omitempty"`
	Content   string `json:"content,omitempty"`
	Category  string `json:"category,omitempty"`
	Published *bool  `json:"published,omitempty"`
}

In the note.model.go file, you can see the CreateNoteSchema and UpdateNoteSchema structs, which we’ll use to validate the incoming data when creating or updating records. Normally, these validation schemas would be in a separate file called note.schema.go, but for simplicity, we’ve included them in the model file.

Additionally, we have a ValidateStruct function that allows us to validate the request bodies using the validation schemas and return any relevant validation errors.

Load the Environment Variables with Viper

To make the environment variables in the app.env file accessible to our application, we can use the Viper package. Let’s create a utility function that will handle this task. Start by creating an ‘initializers‘ folder in the project’s root directory. Within this folder, create a file called loadEnv.go and add the following code to it.

initializers/loadEnv.go


package initializers

import (
	"github.com/spf13/viper"
)

type Config struct {
	DBHost         string `mapstructure:"MYSQL_HOST"`
	DBUserName     string `mapstructure:"MYSQL_USER"`
	DBUserPassword string `mapstructure:"MYSQL_PASSWORD"`
	DBName         string `mapstructure:"MYSQL_DATABASE"`
	DBPort         string `mapstructure:"MYSQL_PORT"`

	ClientOrigin string `mapstructure:"CLIENT_ORIGIN"`
}

func LoadConfig(path string) (config Config, err error) {
	viper.AddConfigPath(path)
	viper.SetConfigType("env")
	viper.SetConfigName("app")

	viper.AutomaticEnv()

	err = viper.ReadInConfig()
	if err != nil {
		return
	}

	err = viper.Unmarshal(&config)
	return
}

Create a Function to Connect to the MySQL Server

Now that we have successfully read the environment variables from the app.env file, we can proceed to establish a connection between our application and the MySQL server using GORM. Once connected, we can migrate the database model to the MySQL database.

To achieve this, create a new file named db.go inside the initializers folder and add the following code to it. This will set up the database connection and migrate the Note model to the MySQL database.

initializers/db.go


package initializers

import (
	"fmt"
	"log"
	"os"

	"github.com/wpcodevo/golang-fiber-mysql/models"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

var DB *gorm.DB

func ConnectDB(config *Config) {
	var err error
	// dsn := fmt.Sprintf("user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local")
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", config.DBUserName, config.DBUserPassword, config.DBHost, config.DBPort, config.DBName)

	DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		log.Fatal("Failed to connect to the Database! \n", err.Error())
		os.Exit(1)
	}

	DB.Logger = logger.Default.LogMode(logger.Info)

	log.Println("Running Migrations")
	DB.AutoMigrate(&models.Note{})

	log.Println("🚀 Connected Successfully to the Database")
}

Let’s put everything together and make sure that our application is set up correctly by testing if we can connect to the MySQL server and sync our GORM model with the MySQL database schema.

To achieve this, we will make some changes to the main.go file. Firstly, let’s create an init function and call the initializers.LoadConfig() function to load the environment variables. This will be followed by calling the initializers.ConnectDB() function to establish a connection pool between the MySQL server and the Fiber server.

Here is the updated code that you can replace the main.go file with:

main.go


package main

import (
	"log"

	"github.com/gofiber/fiber/v2"
	"github.com/wpcodevo/golang-fiber-mysql/initializers"
)

func init() {
	config, err := initializers.LoadConfig(".")
	if err != nil {
		log.Fatalln("Failed to load environment variables! \n", err.Error())
	}
	initializers.ConnectDB(&config)
}

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

	app.Get("/api/healthchecker", func(c *fiber.Ctx) error {
		return c.Status(200).JSON(fiber.Map{
			"status":  "success",
			"message": "Welcome to Golang, Fiber, MySQL, and GORM",
		})
	})

	log.Fatal(app.Listen(":8000"))
}

Once you’ve updated the main.go file as described, ensure that the MySQL server is still running, and then start the Fiber server once again. This time, you should see GORM executing a series of queries in the terminal.

If there are no errors, it means you’ve successfully connected to the MySQL server and synchronized the GORM model with the database schema.

Implement the CRUD Functionality

Now that we have successfully connected the Fiber server to the MySQL database, it’s time to create route controllers that will allow us to perform CRUD (Create, Read, Update, Delete) operations on the database using GORM. These route handlers will act as higher-level functions, building on the lower-level CRUD functionality provided by GORM.

Fiber Route Handler to Create a New Record

This route handler is designed to handle incoming POST requests to the /api/notes endpoint and add new records to the database.

When called, the function first parses the request body and validates the incoming data. If there are any parsing or validation errors, the function will return a JSON response with the corresponding errors.

Assuming the incoming data passes validation, the function creates a new instance of the Note struct, populates it with data from the parsed CreateNoteSchema struct, and timestamps it with the current time.

Next, the function uses the Create method of the DB instance provided by GORM to insert the new data into the MySQL database. If there is an error during the insertion, such as a duplicate entry error, the function will return a JSON response with the appropriate error.

If the insertion is successful, the function will return a JSON response with a 201 Created status code and the newly created note data in the response body.

controllers/note.controller.go


func CreateNoteHandler(c *fiber.Ctx) error {
	var payload *models.CreateNoteSchema

	if err := c.BodyParser(&payload); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": err.Error()})
	}

	errors := models.ValidateStruct(payload)
	if errors != nil {
		return c.Status(fiber.StatusBadRequest).JSON(errors)

	}

	now := time.Now()
	newNote := models.Note{
		Title:     payload.Title,
		Content:   payload.Content,
		Category:  payload.Category,
		Published: payload.Published,
		CreatedAt: now,
		UpdatedAt: now,
	}

	result := initializers.DB.Create(&newNote)

	if result.Error != nil && strings.Contains(result.Error.Error(), "Duplicate entry") {
		return c.Status(fiber.StatusConflict).JSON(fiber.Map{"status": "fail", "message": "Title already exist, please use another title"})
	} else if result.Error != nil {
		return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"status": "error", "message": result.Error.Error()})
	}

	return c.Status(fiber.StatusCreated).JSON(fiber.Map{"status": "success", "data": fiber.Map{"note": newNote}})
}

Fiber Route Handler to Retrieve All Records

In order to efficiently handle large amounts of data in our application, we’ll create a route handler that retrieves a paginated list of records from the MySQL database. By implementing pagination, we can ensure that we’re not sending excessive amounts of data to the user.

This route handler is triggered when a GET request is made to the /api/notes endpoint. The function first reads the page and limit query parameters from the request. If these parameters are not provided, they will default to page 1 and limit 10.

controllers/note.controller.go


func FindNotes(c *fiber.Ctx) error {
	var page = c.Query("page", "1")
	var limit = c.Query("limit", "10")

	intPage, _ := strconv.Atoi(page)
	intLimit, _ := strconv.Atoi(limit)
	offset := (intPage - 1) * intLimit

	var notes []models.Note
	results := initializers.DB.Limit(intLimit).Offset(offset).Find(&notes)
	if results.Error != nil {
		return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"status": "error", "message": results.Error})
	}

	return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "success", "results": len(notes), "notes": notes})
}

Next, the function converts these parameters to integers and calculates the offset for the query. It then uses the Find method of the DB instance provided by GORM to retrieve the relevant notes, limiting the results to the specified page and offset.

Finally, the handler sends a JSON response with a 200 OK status code and the paginated list of notes, along with the total number of notes returned in the current page. If there is an error during the query, the function returns a JSON response with the appropriate error.

Fiber Route Handler to Edit a Record

This route handler is responsible for editing records in the database when a PATCH request is made to the /api/notes/:noteId endpoint.

Upon receiving a request, the function first extracts the noteId parameter from the request. It then attempts to parse the request body, returning a JSON response with a status code of 400 (Bad Request) and an error message if parsing fails.

Next, the function queries the database to check if a record with the given ID exists. If the query fails or no record is found, the function returns a JSON response with an error message and a status code of either 404 (Not Found) or 502 (Bad Gateway).

controllers/note.controller.go


func UpdateNote(c *fiber.Ctx) error {
	noteId := c.Params("noteId")

	var payload *models.UpdateNoteSchema

	if err := c.BodyParser(&payload); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": err.Error()})
	}

	var note models.Note
	result := initializers.DB.First(&note, "id = ?", noteId)
	if err := result.Error; err != nil {
		if err == gorm.ErrRecordNotFound {
			return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"status": "fail", "message": "No note with that Id exists"})
		}
		return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"status": "fail", "message": err.Error()})
	}

	updates := make(map[string]interface{})
	if payload.Title != "" {
		updates["title"] = payload.Title
	}
	if payload.Category != "" {
		updates["category"] = payload.Category
	}
	if payload.Content != "" {
		updates["content"] = payload.Content
	}

	if payload.Published != nil {
		updates["published"] = payload.Published
	}

	updates["updated_at"] = time.Now()

	initializers.DB.Model(&note).Updates(updates)

	return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "success", "data": fiber.Map{"note": note}})
}

If the query is successful, the function creates a updates map to hold the updated fields of the note record. This is because the handler is for a PATCH request, and the user may only want to update certain fields.

The function then uses the Updates method of the gorm.DB object to update the note in the database with the fields in the updates map.

Finally, the function returns a JSON response with a status code of 200 (OK) and the updated note in the response body.

Fiber Route Handler to Fetch a Single Record

This route handler retrieves a single note from the database, identified by the noteId parameter in the request. The function first extracts the noteId from the request parameters and uses it to query the database for the corresponding note. If the note is found, the function returns a JSON response with the note data and a status code of 200 (OK).

However, if the requested note is not found, the function returns a JSON response with a status code of 404 (Not Found) and an error message. Similarly, if there is an unexpected error while querying the database, the function returns a JSON response with a status code of 502 (Bad Gateway) and an error message.

controllers/note.controller.go


func FindNoteById(c *fiber.Ctx) error {
	noteId := c.Params("noteId")

	var note models.Note
	result := initializers.DB.First(&note, "id = ?", noteId)
	if err := result.Error; err != nil {
		if err == gorm.ErrRecordNotFound {
			return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"status": "fail", "message": "No note with that Id exists"})
		}
		return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"status": "fail", "message": err.Error()})
	}

	return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "success", "data": fiber.Map{"note": note}})
}

Fiber Route Handler to Delete a Record

The final route function in our API is responsible for permanently deleting a note from the database. This function is triggered by a DELETE request to the /api/notes/:noteId endpoint.

When called, the function extracts the noteId parameter from the request and queries the database to find the corresponding record. If the record exists, the function deletes it from the database. If no record is deleted, it means that there was no note in the database with the specified ID, so the function returns a 404 error.

If the deletion is successful, the function returns a 204 (No Content) response.

controllers/note.controller.go


func DeleteNote(c *fiber.Ctx) error {
	noteId := c.Params("noteId")

	result := initializers.DB.Delete(&models.Note{}, "id = ?", noteId)

	if result.RowsAffected == 0 {
		return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"status": "fail", "message": "No note with that Id exists"})
	} else if result.Error != nil {
		return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"status": "error", "message": result.Error})
	}

	return c.SendStatus(fiber.StatusNoContent)
}

The Complete Code of the Fiber Route Handlers

controllers/note.controller.go


package controllers

import (
	"strconv"
	"strings"
	"time"

	"github.com/gofiber/fiber/v2"
	"github.com/wpcodevo/golang-fiber-mysql/initializers"
	"github.com/wpcodevo/golang-fiber-mysql/models"
	"gorm.io/gorm"
)

func CreateNoteHandler(c *fiber.Ctx) error {
	var payload *models.CreateNoteSchema

	if err := c.BodyParser(&payload); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": err.Error()})
	}

	errors := models.ValidateStruct(payload)
	if errors != nil {
		return c.Status(fiber.StatusBadRequest).JSON(errors)

	}

	now := time.Now()
	newNote := models.Note{
		Title:     payload.Title,
		Content:   payload.Content,
		Category:  payload.Category,
		Published: payload.Published,
		CreatedAt: now,
		UpdatedAt: now,
	}

	result := initializers.DB.Create(&newNote)

	if result.Error != nil && strings.Contains(result.Error.Error(), "Duplicate entry") {
		return c.Status(fiber.StatusConflict).JSON(fiber.Map{"status": "fail", "message": "Title already exist, please use another title"})
	} else if result.Error != nil {
		return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"status": "error", "message": result.Error.Error()})
	}

	return c.Status(fiber.StatusCreated).JSON(fiber.Map{"status": "success", "data": fiber.Map{"note": newNote}})
}

func FindNotes(c *fiber.Ctx) error {
	var page = c.Query("page", "1")
	var limit = c.Query("limit", "10")

	intPage, _ := strconv.Atoi(page)
	intLimit, _ := strconv.Atoi(limit)
	offset := (intPage - 1) * intLimit

	var notes []models.Note
	results := initializers.DB.Limit(intLimit).Offset(offset).Find(&notes)
	if results.Error != nil {
		return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"status": "error", "message": results.Error})
	}

	return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "success", "results": len(notes), "notes": notes})
}

func UpdateNote(c *fiber.Ctx) error {
	noteId := c.Params("noteId")

	var payload *models.UpdateNoteSchema

	if err := c.BodyParser(&payload); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": err.Error()})
	}

	var note models.Note
	result := initializers.DB.First(&note, "id = ?", noteId)
	if err := result.Error; err != nil {
		if err == gorm.ErrRecordNotFound {
			return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"status": "fail", "message": "No note with that Id exists"})
		}
		return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"status": "fail", "message": err.Error()})
	}

	updates := make(map[string]interface{})
	if payload.Title != "" {
		updates["title"] = payload.Title
	}
	if payload.Category != "" {
		updates["category"] = payload.Category
	}
	if payload.Content != "" {
		updates["content"] = payload.Content
	}

	if payload.Published != nil {
		updates["published"] = payload.Published
	}

	updates["updated_at"] = time.Now()

	initializers.DB.Model(&note).Updates(updates)

	return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "success", "data": fiber.Map{"note": note}})
}

func FindNoteById(c *fiber.Ctx) error {
	noteId := c.Params("noteId")

	var note models.Note
	result := initializers.DB.First(&note, "id = ?", noteId)
	if err := result.Error; err != nil {
		if err == gorm.ErrRecordNotFound {
			return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"status": "fail", "message": "No note with that Id exists"})
		}
		return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"status": "fail", "message": err.Error()})
	}

	return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "success", "data": fiber.Map{"note": note}})
}

func DeleteNote(c *fiber.Ctx) error {
	noteId := c.Params("noteId")

	result := initializers.DB.Delete(&models.Note{}, "id = ?", noteId)

	if result.RowsAffected == 0 {
		return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"status": "fail", "message": "No note with that Id exists"})
	} else if result.Error != nil {
		return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"status": "error", "message": result.Error})
	}

	return c.SendStatus(fiber.StatusNoContent)
}

Create the API Routes and Setup CORS

Now that we have implemented all the API route handlers, it’s time to bring them together and define routes for our API. Additionally, we’ll configure CORS middleware to allow cross-origin resource sharing for specific origins, headers, methods, and credentials.

To achieve this, let’s open the main.go file and replace its current content with the following code snippets.

main.go


package main

import (
	"log"

	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/middleware/cors"
	"github.com/gofiber/fiber/v2/middleware/logger"
	"github.com/wpcodevo/golang-fiber-mysql/controllers"
	"github.com/wpcodevo/golang-fiber-mysql/initializers"
)

func init() {
	config, err := initializers.LoadConfig(".")
	if err != nil {
		log.Fatalln("Failed to load environment variables! \n", err.Error())
	}
	initializers.ConnectDB(&config)
}

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

	app.Mount("/api", micro)
	app.Use(logger.New())
	app.Use(cors.New(cors.Config{
		AllowOrigins:     "http://localhost:3000",
		AllowHeaders:     "Origin, Content-Type, Accept",
		AllowMethods:     "GET, POST, PATCH, DELETE",
		AllowCredentials: true,
	}))

	micro.Route("/notes", func(router fiber.Router) {
		router.Post("/", controllers.CreateNoteHandler)
		router.Get("", controllers.FindNotes)
	})
	micro.Route("/notes/:noteId", func(router fiber.Router) {
		router.Delete("", controllers.DeleteNote)
		router.Get("", controllers.FindNoteById)
		router.Patch("", controllers.UpdateNote)
	})
	micro.Get("/healthchecker", func(c *fiber.Ctx) error {
		return c.Status(200).JSON(fiber.Map{
			"status":  "success",
			"message": "Welcome to Golang, Fiber, MySQL, and GORM",
		})
	})

	log.Fatal(app.Listen(":8000"))
}

To start the application again with the changes made to the main.go file, you have two options: run the command go run main.go or use air. If you encounter any errors in the terminal, it’s advisable to review the previous steps to locate the source of the errors.

Test the CRUD API with a Frontend

Now that we have completed the implementation of the CRUD API, it’s time to test the endpoints to ensure that they function correctly. You can perform this test using any API testing software such as Postman or the Thunder Client VS Code extension.

To access the collection used in testing the API, download the project from https://github.com/wpcodevo/golang-fiber-mysql and import the Note App.postman_collection.json file into either Thunder Client or Postman.

Once you’ve imported the collection, you can make requests to the Fiber server to test the CRUD functionality of the Golang API.

Alternatively, you can use a frontend application built with React to test the API. To spin up the React CRUD app in seconds, follow the instructions outlined in the “Run the CRUD API with a Frontend App” section.

Perform the CREATE Operation of CRUD

To create a new note, just click on the plus icon or the “Add new note” button, and a popup modal will appear, allowing you to enter the note’s title and content. Once you’ve filled out the form, click on the “Create Note” button to submit the data to the Golang API.

If the request is successful, the newly created note will be displayed. However, if an error occurs, an alert notification will be shown to indicate the issue.

react.js crud app create a new record with an api

Perform the UPDATE Operation of CRUD

To edit a note, you can access the note’s settings menu by clicking on the three dots located on the note item. From there, choose the Edit option and a popup modal will appear where you can make the desired changes.

Once you have made the necessary modifications, click on the ‘Update Note‘ button to submit the updated data to the Golang API. If the request is successful, the changes will be reflected in the note.

react.js crud app update an existing record against a restful api

Perform the READ Operation of CRUD

Now, let’s move on to testing the READ operation, which occurs automatically when you land on the root route of the application. A GET request will be sent to retrieve a paginated list of notes from the Golang API. If the request is successful, the list of note items will be displayed.

react.js crud app retrieve all records in the database via a restful api

Perform the DELETE Operation of CRUD

Let’s wrap up by testing the DELETE operation. To delete a note, simply click on the three dots on the note item and select the “Delete” option from the settings menu that appears. A confirmation prompt will appear to ensure that you want to proceed since the delete operation is irreversible.

After confirming the deletion, a DELETE request will be made to the Golang API to remove the note from the database. If the request is successful, the note item will be immediately removed from the page.

react.js crud app delete a record in the database

Conclusion

We’ve reached the end of this tutorial! You can access the source code for the project on the Golang CRUD API with GORM, Fiber, and MySQL repository on GitHub.

Throughout this tutorial, we explored how to create a Golang CRUD API using GORM, MySQL, and the Fiber framework. I hope that you found this tutorial helpful and engaging. If you have any questions or feedback, please feel free to leave a comment below.