In this article, you’ll learn how to build a CRUD API in Golang using the Fiber web framework and GORM. The REST API will run on a Fiber HTTP server and use GORM to persist data in a PostgreSQL database.

What is Fiber? Fiber is a fast and lightweight web framework for building blazingly-fast web applications and APIs in Golang. Fiber’s architecture and syntax are heavily influenced by the Express.js web framework in Node.js, making it familiar and easy to use for those with prior experience with Express.

What is GORM? Golang Object-Relational Mapping, also commonly referred to as GORM, is an open-source ORM library that allows developers to interact with SQL databases using a clean and simple API, rather than writing raw SQL queries.

An ORM like GORM acts as a bridge between your application’s code and the database, allowing you to interact with data using Go structs and data types, instead of having to write raw SQL queries. At the time of writing this article, GORM supports various database servers such as MySQL, PostgreSQL, SQLite, and SQL server, and can be easily configured to work with other databases as well.

More practice:

Create CRUD API in Golang using Fiber and GORM

Prerequisites

  • Prior knowledge of any Golang web framework like Gin Gonic or Express.js will be beneficial.
  • You should have Docker installed. This is needed to run the Postgres and pgAdmin servers in the Docker containers.
  • You should have a basic understanding of Golang and its ecosystem.

Run the Golang Fiber Project Locally

  • Download or clone the Golang Fiber CRUD project from https://github.com/wpcodevo/golang-fiber and open the source code in a code editor.
  • Open the integrated terminal in your IDE or text editor and run the command docker-compose up -d to start the Postgres and pgAdmin servers in the containers.
  • Run the command go run main.go to install the necessary packages, migrate the GORM schema to the Postgres database, and start the Fiber HTTP server.
  • To test the CRUD endpoints, import the Note App.postman_collection.json file into Postman and make HTTP requests to the Fiber server. Alternatively, you can set up the frontend application to interact with the API.

Run the Fiber API with React.js App

For a detailed walkthrough on how to build the CRUD app in React.js, see the post Build a React.js CRUD App using a RESTful API. Nonetheless, use the following steps to spin up the React application without writing a single line of code.

  • If you haven’t already, please ensure that you have Node.js and Yarn installed on your machine.
  • Obtain 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 preferred IDE or text editor.
  • Navigate to the root directory in the console and run the command yarn or yarn install to install the project’s dependencies.
  • Once the dependencies have been installed, run the command yarn dev to start the Vite development server.
  • To test the CRUD functionalities, open the app in your browser at http://localhost:3000/. It is important to note that visiting the React app at http://127.0.0.1:3000 may result in site can’t be reached or CORS errors, therefore it is best to use http://localhost:3000/.

Setup the Golang Project

Upon completing this guide, your folder structure will resemble the screenshot below, excluding the Makefile and Note App.postman_collection.json files.

golang, gorm, and fiber crud api project structure

Start by going to the go/src folder or any preferred location on your machine and create a folder named golang-fiber.

Then, navigate into the newly-created folder and run go mod init to initialize the Golang project. Don’t forget to replace the wpcodevo with your GitHub username.


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

After that, open the folder in an IDE or text editor and execute these commands to install the project dependencies.


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/postgres
go get github.com/spf13/viper
go install github.com/cosmtrek/air@latest

  • fiber – A web framework for Golang.
  • uuid – A library to generate and parse UUIDs in Golang.
  • validator – A library that provides an interface for validating Go structs, values, and variables.
  • gorm – A powerful and convenient library for database interactions in Golang.
  • postgres – A Postgres driver for GORM
  • viper – A library for managing configuration files in Golang.
  • air – This library allows you to hot-reload the server when required files change.

Let’s get our hands dirty by creating a basic Fiber HTTP server that returns a simple JSON object when a GET request is made to the /api/healthchecker endpoint.

To do that, create a main.go file in the root directory and add the following code.

main.go


package main

import (
	"log"

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

func main() {
	app := fiber.New()
	app.Use(logger.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"))
}

In the above code, we imported the necessary packages at the top of the file and created a new instance of Fiber by calling fiber.New(). Then, we registered the HTTP middleware logger and defined a health checker route with a GET method that listens to the /api/healthchecker endpoint.

When the route receives the request, it will call the Fiber context handler to return the JSON object. Finally, we started the server on port 8000 by calling app.Listen(":8000").

Since you have installed the https://github.com/cosmtrek/air binary, you can run air to start the Fiber HTTP server. If you do not want to hot-reload the Go application, you can run the command go run main.go to start the server.

Make a GET request to the http://localhost:8000/api/healthchecker URL to see the JSON object.

Testing the health checker route of the golang application

Setup Postgres and pgAdmin with Docker

Now let’s set up the PostgreSQL and pgAdmin servers with Docker. In the root directory, create a docker-compose.yml file and add the following Docker compose configurations.

docker-compose.yml


version: '3'
services:
  postgres:
    image: postgres:latest
    container_name: postgres
    ports:
      - '6500:5432'
    volumes:
      - progresDB:/data/postgres
    env_file:
      - ./app.env
  pgAdmin:
    image: dpage/pgadmin4
    container_name: pgAdmin
    env_file:
      - ./app.env
    ports:
      - "5050:80"
volumes:
  progresDB:


The postgres service will retrieve the most recent Postgres image from DockerHub, start the Postgres server within the container, and make the container accessible through port 6500.

In a similar manner, the pgAdmin service will pull the dpage/pgadmin4 image from DockerHub, launch pgAdmin inside the container, and make the container accessible via port 5050.

Since we are sourcing the Postgres and pgAdmin credentials from an app.env file via the env_file property, it is necessary to create the file to make the environment variables available to Docker Compose.

To do this, create an app.env file in the root directory and add the following environment variables.

app.env


POSTGRES_HOST=127.0.0.1
POSTGRES_PORT=6500
POSTGRES_USER=admin
POSTGRES_PASSWORD=password123
POSTGRES_DB=golang_fiber

DATABASE_URL=postgresql://admin:password123@localhost:6500/golang_fiber?schema=public

CLIENT_ORIGIN=http://localhost:3000

PGADMIN_DEFAULT_EMAIL=admin@admin.com
PGADMIN_DEFAULT_PASSWORD=password123

With that out of the way, run the command below to start the Postgres and pgAdmin containers.


docker-compose up -d

You can open the Docker desktop application to see the running Postgres and pgAdmin containers.

check docker desktop to see the running postgres and pgadmin containers

Design the Database Model with GORM

Now that the Postgres server is running inside the Docker container, let’s design the database model with GORM. The model represents a SQL table, while the fields represent the columns.

To prevent the user from sending junk values in the request body, we’ll use struct tags along with the https://github.com/go-playground/validator package to define validation rules on the struct fields.

Create a models folder in the root directory and create a note.model.go within it. Open the models/note.model.go file and add the following model definitions.

models/note.model.go


package models

import (
	"time"

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

type Note struct {
	ID        uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4();primary_key" json:"id,omitempty"`
	Title     string    `gorm:"varchar(255);uniqueIndex;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" json:"createdAt,omitempty"`
	UpdatedAt time.Time `gorm:"not null" json:"updatedAt,omitempty"`
}

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 code above, we created a ValidateStruct function that will take a reference to a validation schema, validate the request body against the rules defined in the schema, and return relevant validation errors to the client.

Load Environment Variables and Connect to Postgres

In this section, you will create two functions – LoadConfig and ConnectDB. The LoadConfig function will load the environment variables from the app.env file and make them available throughout the application.

The ConnectDB function on the other hand will connect the application to the running Postgres server and migrate the GORM model to the Postgres database.

Load the Environment Variables

Here, we’ll use the https://github.com/spf13/viper package to read the environment variables from the app.env file and make them available in the Golang runtime.

To do this, create an initializers folder in the root directory. Then, within the initializers folder, create a loadEnv.go file and add the following code.

initializers/loadEnv.go


package initializers

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

type Config struct {
	DBHost         string `mapstructure:"POSTGRES_HOST"`
	DBUserName     string `mapstructure:"POSTGRES_USER"`
	DBUserPassword string `mapstructure:"POSTGRES_PASSWORD"`
	DBName         string `mapstructure:"POSTGRES_DB"`
	DBPort         string `mapstructure:"POSTGRES_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
}

The LoadConfig function will read the environment variables from the app.env file and unmarshal the content into the Config struct.

Connect to the Postgres Server

Now let’s create a ConnectDB function to create connection pools between the application and the running PostgreSQL server. Since we are using the uuid_generate_v4() function as a default value for the id column, we need to install the PostgreSQL uuid-ossp module for this to work.

To do this, we’ll use the DB.Exec() method provided by GORM to execute raw SQL to create the extension if it doesn’t exist. After that, we’ll use the DB.AutoMigrate() method to migrate the GORM model to the Postgres database.

initializers/db.go


package initializers

import (
	"fmt"
	"log"
	"os"

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

var DB *gorm.DB

func ConnectDB(config *Config) {
	var err error
	dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Shanghai", config.DBHost, config.DBUserName, config.DBUserPassword, config.DBName, config.DBPort)

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

	DB.Exec("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"")
	DB.Logger = logger.Default.LogMode(logger.Info)

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

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

Database Migration with GORM

By incorporating the above configurations, let’s create the init() function in the main.go file to call the LoadConfig and ConnectDB functions. Evoking the ConnectDB function will update the schema of the database to match the structure of the GORM model.

main.go


package main

import (
	"log"

	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/middleware/logger"
	"github.com/wpcodevo/golang-fiber/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.Use(logger.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"))
}

Ensure that the Postgres server is running and then start the Fiber HTTP server by running either air or go run main.go.

Next, let’s establish a connection to the Postgres server using a Postgres client to see the SQL table generated by GORM. First, we need to obtain the IP address of the Postgres container.

To get the IP address, run the command docker inspect postgres in the terminal and scroll down to the “NetworkSettings” section. In the “Networks” JSON object, copy the value of the “IPAddress” property.

Next, access the pgAdmin login page at http://localhost:5050/ and sign in using the credentials specified in the environment variables file. After signing into the pgAdmin dashboard, register the Postgres server by providing the following credentials:

  • Host name/address – Enter the IP address you copied.
  • Port – Enter 5143
  • Maintenance database – Enter postgres
  • Password – Enter the Postgres password specified in the app.env file.
register the postgres server in pgadmin

Next, inspect the golang_fiber database to see the SQL table created by GORM.

sql table created by gorm

Implement the CRUD Functionalities

At this point, we’re ready to create higher-level CRUD functions to perform the CRUD operations against the database by utilizing the lower-level CRUD functions provided by GORM.

Add New Record

First, we’ll create a Fiber context handler to perform a CREATE operation on the database. When a POST request is made to the /api/notes endpoint, Fiber will call this route function to insert the new data into the database.

when the route handler receives the request, the models.ValidateStruct() function will be evoked to validate the incoming data against the rules defined in the models.CreateNoteSchema struct.

Upon the absence of validation errors, GORM’s DB.Create() method will be executed to insert the new data into the database. If the operation is successful, the newly-inserted record will be returned in the JSON response.

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 key value violates unique") {
		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}})
}

If a record with that title already exists in the database, a 409 Conflict error response will be sent to the client.

Fetch All Records

Let’s create a Fiber context handler to return a paginated list of records to the client. When Fiber calls this route function, it will extract the page and limit parameters from the query string. If either of them is missing, default values of 1 and 10 will be assigned to them respectively.

Next, the strconv.Atoi() function will be called to convert the page and limit parameters to integers. Then, the offset value will be called and assigned to an offset variable.

After that, GORM’s DB.Limit().Offset() methods will be called to paginate the results and the .Find() method will be evoked to retrieve all the records that match the query.

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})
}

If the operation fails, a 500 Bad Gateway response will be sent to the client. Otherwise, the selected records will be returned in the JSON response.

Edit a Record

Here, you’ll perform the UPDATE operation to edit the fields of a record in the database. This route function will retrieve the record’s ID from the URL parameter and call the DB.First() method to check if a record with that ID exists in the database.

If no record was found, a 404 Not Found error will be sent to the client. Otherwise, the DB.Model().Updates() method will be called to update the fields of the record based on the request payload.

controllers/note.controller.go


unc 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 operation is successful, the newly-updated record will be sent to the client in the JSON response.

Retrieve a Single Record

Now let’s perform the second READ operation to retrieve a single record from the database. When this Fiber context handler is called, it will extract the record’s ID from the URL parameter, and evoke the DB.First() method to retrieve the record that matches the ID.

If no record with that ID exists, a 404 Not Found response will be sent. Otherwise, the found record will be returned in the JSON response.

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}})
}

Delete a Record

This Fiber context handler will be called to delete a record from the database when a DELETE request is made to the /api/notes/:noteId endpoint. When Fiber calls this route function, it will extract the record’s ID from the request parameter and evoke the DB.Delete() method to remove the record that matches the ID.

If the number of affected rows is equal to zero, a 404 Not Found error response will be sent to the client. Otherwise, a 204 No Content status code will be returned.

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)
}

Complete Fiber Route Functions

controllers/note.controller.go


package controllers

import (
	"strconv"
	"strings"
	"time"

	"github.com/gofiber/fiber/v2"
	"github.com/wpcodevo/golang-fiber/initializers"
	"github.com/wpcodevo/golang-fiber/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 key value violates unique") {
		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 Add CORS

Now that we’ve defined all the Fiber route handlers, let’s create routes for them and configure the server with CORS. By adding CORS, we can specify which origins are authorized to access the server’s resources and the HTTP methods that should be included in the requests.

To accomplish this, replace the content of the main.go file with the code below.

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/controllers"
	"github.com/wpcodevo/golang-fiber/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, and GORM",
		})
	})

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

Finally, start the Fiber HTTP server by running air or go rum main.go.

Conclusion

If you made it this far, am proud of you. You can find the complete source code of the Golang Fiber CRUD API project on GitHub.

In this article, we built a CRUD API in Golang using GORM, PostgreSQL, and the Fiber web framework. The application provides complete functionality such as adding a new record, editing an existing record, retrieving a single record, fetching all records, and deleting a record.

I hope you found this article informative. Don’t hesitate to leave a comment if you have any questions.