Have you been wanting to build a simple API in Golang, but don’t know where to start? Look no further than this tutorial, where I’ll guide you through the process step by step.

Why choose Fiber framework? Fiber is a highly performant, flexible, and user-friendly Golang web framework that has been gaining popularity. Its interface is similar to that of Express.js, a popular Node.js web framework, so if you have experience working with Express, you’ll feel right at home with Fiber.

What is GORM? GORM is a powerful ORM library for Golang that supports various databases, such as SQLite, MySQL, PostgreSQL, and SQL Server. With GORM, developers can enjoy a fluent API, support for complex queries, database migrations, associations, and validations.

By combining Fiber and GORM, you’ll have all the necessary tools to build a robust API in Golang that can handle your CRUD operations. Additionally, I’ll demonstrate how to use a SQLite database to store your data, allowing you to get started with minimal setup.

Whether you’re a seasoned developer looking to expand your skill set or a newcomer to Golang, this tutorial is the perfect starting point for building your own API. So, let’s dive in and get started!

How To Upload Single and Multiple F...
How To Upload Single and Multiple Files in Golang

More practice:

Build a Simple API in Golang using Fiber and SQLite

Run the Golang API on your Computer

To run the Golang SQLite API project on your local machine, follow these simple steps:

  1. Start by downloading or cloning the Golang SQLite CRUD project from the GitHub repository at https://github.com/wpcodevo/golang-fiber-sqlite. Once you have the project, open it in your preferred IDE or text editor.
  2. Next, go to your terminal and execute the go run main.go command. This will install all the necessary packages, migrate the GORM schema to the SQLite database, and start the Fiber HTTP server.
  3. To test the CRUD endpoints, you can use Postman to make HTTP requests to the Fiber server by importing the Note App.postman_collection.json file. Alternatively, you can follow the instructions below if you prefer to interact with the API using a frontend application.

Run the Golang API with a Frontend App

If you’re looking for a more detailed guide on how to build a React.js CRUD app, you can check out the post titled “Build a React.js CRUD App using a RESTful API“. However, if you want to get started without having to write any code, follow the steps below:

  1. First, ensure that you have Node.js and Yarn installed on your machine. If not, you can download and install them from the official websites.
  2. Next, 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 then open it in your preferred IDE or text editor.
  3. In the console, navigate to the root directory of the project and execute 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 by navigating to http://localhost:3000/. Keep in mind that accessing the React app at http://127.0.0.1:3000 might result in a “site can’t be reached” error or CORS errors. Therefore, it’s best to use http://localhost:3000/.

Setup the Golang Project

Before we begin with the project setup, take a moment to preview the expected folder and file structure in the screenshot below. This will give you a better understanding of the tutorial’s end goal.

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

To begin, choose a directory on your computer where you’d like the project files to be stored, and open a terminal there. Then, type or copy/paste the following commands into the terminal to create a new folder called golang-fiber-sqlite and set up a Golang project within that folder.


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

After initializing the Golang project, open it in your favorite IDE or text editor. Once you have the project open, you can access the integrated terminal within your IDE. In the terminal, run the following commands to install the necessary dependencies for the project.


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/sqlite
go install github.com/cosmtrek/air@latest

  • fiber: A fast and flexible web framework for Golang that supports middleware, routing, error handling, and many other common web features.
  • uuid: A library for generating and parsing UUIDs in Golang.
  • validator: A library for validating Go structs, values, and variables.
  • gorm: A popular ORM library for Golang that supports automatic database schema generation, association handling, and query building.
  • sqlite: A SQLite driver for gorm.
  • air: A library that automatically rebuilds and restarts your application for faster development.

With all the necessary dependencies installed, we can now create a basic Fiber server to get started. Our server will have a single endpoint – a health checker – which will return a JSON object containing a message. To do this, we need to 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"
)

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, SQLite, and GORM",
		})
	})

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

To save time from manually restarting the server every time we modify the code, we’ll leverage air, a tool that monitors the source code and automatically triggers the rebuild process when necessary. Since we’ve already installed the air binary, just run the air command in your terminal to start the Fiber HTTP server.

Once the server is running, you can test it by sending a GET request to http://localhost:8000/api/healthchecker and inspecting the returned JSON object.

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

Create the GORM Database Model

Let’s move on to creating the GORM model, which will allow us to generate a corresponding SQL table in our SQLite database. To keep things simple, we will only have one GORM model.

GORM models are straightforward to declare, as they are just regular Golang structs with basic types, pointers/aliases of those types, or custom types implementing the Scanner and Valuer interfaces.

However, since SQLite does not have a built-in UUID type, we will utilize GORM’s BeforeCreate hook along with the github.com/google/uuid package to generate a UUID for each record before it is inserted into the database. The BeforeCreate hook is executed before GORM executes the INSERT query to insert the data into the database. You can learn more about the various hooks at https://gorm.io/docs/hooks.html.

To accomplish this, create a ‘models‘ folder in the root directory, then within the ‘models‘ folder, create a note.model.go file and add the following code.

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"`
}

While reviewing the note.model.go file, you may have noticed that it contains not only the GORM model, but also other pieces of code such as the ValidateStruct function, as well as the CreateNoteSchema and UpdateNoteSchema structs.

Although it’s generally good practice to separate these components into different files for better organization, we’ve decided to keep them in the same file to keep the project simple and avoid having too many moving parts.

These additional components are important for ensuring that requests to create or update notes are properly validated before they’re saved to the database. The CreateNoteSchema and UpdateNoteSchema contain the required fields and validation rules for creating and updating notes respectively. The ValidateStruct function utilizes these schemas to validate the request bodies and provide relevant error messages in case of any validation failures.

So while they may add a bit of extra code to the note.model.go file, they are critical for the proper functioning of the project.

Connect to the SQLite Database

To connect the Fiber server to the SQLite database, we need to create a utility function that initializes the database. This function generates the SQLite database file if it doesn’t exist and then executes the AutoMigrate function to create the SQL table. To create the ConnectDB function, create a new file named db.go in the ‘initializers‘ folder located in the project’s root directory, and add the following code:

initializers/db.go


package initializers

import (
	"log"
	"os"

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

var DB *gorm.DB

func ConnectDB() {
	var err error

	DB, err = gorm.Open(sqlite.Open("gorm.db"), &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")
}

This function will connect to the SQLite database and then run the necessary migrations to ensure that the database is up to date with the latest schema.

With the ConnectDB function ready to use, you can now create an init function in main.go file and call it. When init is called, ConnectDB will run, connecting the Fiber app to the SQLite database.

main.go


package main

import (
	"log"

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

func init() {
	initializers.ConnectDB()
}

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, SQLite, and GORM",
		})
	})

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


Now that everything is set up, it’s time to start the Fiber HTTP server again. As it starts up, you can observe GORM executing a series of queries in the terminal. These queries are responsible for generating the SQLite database, creating the SQL table, and performing other necessary tasks. Additionally, you should be able to locate the SQLite database file in the root directory.

Implement the CRUD Fiber Functions

At this point, we’ve successfully defined the GORM database model, established a connection to the SQLite database, and synchronized the GORM model with the database schema. With these foundational steps in place, it’s time to move on to implementing the core functionality of our application: CRUD operations.

To achieve this, we will create route handlers with Fiber that act as higher-level CRUD functions. These handlers will utilize the lower-level CRUD functions provided by GORM to interact with the SQLite database.

Route Controller to Add New Records

This route handler is responsible for the CREATE operation of CRUD. When a POST request is made to the /api/notes endpoint, it will be invoked to add a new note to the SQLite database.

Upon invocation, the request body will be parsed into the models.CreateNoteSchema struct, and the models.ValidateStruct() function will be used to validate the request payload. If any errors are found, a BadRequest response with the error messages will be sent back to the client.

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

The function will then create a new Note instance using the data from the parsed request payload and set the CreatedAt and UpdatedAt fields to the current time.

Subsequently, the Create method of the DB instance from the ‘initializers‘ package will be used to insert the new Note into the database. If a note with that title already exists in the database, a Conflict error will be returned.

However, if the operation is successful, the newly-inserted note will be returned in the response body as a success status along with the newly created note data in a data field.

Route Controller to Fetch All Records

To implement the READ operation of CRUD, we’ll create a route handler that retrieves a list of notes from the SQLite database and returns them as a JSON response when a GET request is made to the /api/notes?page=1&limit=10 endpoint.

Upon invocation, the handler function will first read the page and limit query parameters from the incoming GET request, which will determine the number of results to be returned and the page to start the pagination. If these parameters are not provided in the request, default values of 1 for page and 10 for limit will be used.

Next, the function will convert the page and limit query parameter values from strings to integers using the strconv.Atoi() function. Based on these values, it will calculate the offset value that specifies the starting point of the query results.

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

The function will then use the Find() method of the DB instance from the initializers package to retrieve a list of notes from the database. This method uses the provided limit and offset values to limit the number of results returned and paginate through the records.

If an error occurs during this process, the function returns a Bad Gateway response with the error message. Otherwise, a successful response is returned with the list of notes in the response body, along with the total number of notes returned.

Route Controller to Update a Records

This route function is responsible for updating an existing note in the SQLite database when a PATCH request is sent to the /api/notes/:noteId endpoint.

The function will begin by reading the noteId parameter from the request and parsing the request body into the models.UpdateNoteSchema struct. If parsing fails, the function will respond with a Bad Request message.

Next, the function will use the First() method of the DB instance to retrieve the note with the given ID from the database. If the note does not exist, the function will respond with a Not Found message. If there is an error during retrieval, the function will respond with a Bad Gateway message.

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

To allow the user or client to update any field in the note, rather than providing all the fields, the function will create a map of the fields that need to be updated in the note based on the fields in the parsed request payload. The function will also set the UpdatedAt field of the note to the current time.

Finally, the Updates() method of the DB instance will be called to update the note in the database with the provided updates. If the operation is successful, the updated note will be returned in the response body.

Route Controller to Retrieve a Record

Now, we’ll implement a function that retrieves a single record from the database and returns it in a JSON response. This function will handle GET requests made to the /api/notes/:noteId endpoint.

To retrieve the note, the function will first extract the noteId parameter from the request and pass it to the DB.First function. If the note with the given ID doesn’t exist, the function will return a 404 (Not Found) error.

Otherwise, the function will return the found note in a JSON response with a success status code.

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

Route Controller to Delete a Records

As a last step, we’ll implement a route handler for deleting notes from the database. This function will be triggered when a DELETE request is made to the /api/notes/:noteId endpoint.

To begin, the function will extract the ID of the note to be deleted from the request parameter. Then, the DB.Delete() function will be called to remove the note that matches the query from the database.

If no records were deleted, it means that there was no note with that ID in the database, so a 404 (Not Found) error will be returned to the client. On the other hand, if a note was deleted, a 204 (No Content) response will be sent to the client.

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 Controllers

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

Setup CORS and Create the Fiber Routes

To complete our application, we need to register our route handlers with Fiber so that it can use them to handle incoming requests and perform CRUD operations on our database.

Additionally, we need to configure CORS to allow requests from specified origins. Failure to configure CORS can lead to errors when trying to make requests from a frontend application running on a different origin or port.

To accomplish this, we can replace the contents of the main.go file with the following code.

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() {
	initializers.ConnectDB()
}

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, SQLite, and GORM",
		})
	})

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

Now that you have completed building the CRUD functionality for the note-taking app using Fiber, you can start the HTTP server by running either go run main.go or air.

To test the API’s CRUD endpoints, you can download or clone the project from https://github.com/wpcodevo/golang-fiber-sqlite and import the Note App.postman_collection.json file into Postman. This will give you access to a collection of preconfigured requests that you can use to test the endpoints.

If you prefer to use a frontend application to interact with the API, follow the instructions in the “Run the Golang API with a Frontend App” section. This will help you to quickly spin up a frontend app that can communicate with the API and test its functionality.

Conclusion

Throughout this article, you have learned how to build a basic API in Golang with CRUD functionality. Although the API we have built is functional, there are other features we could add to enhance its usability and security, such as JWT-based authentication and access control.

I encourage you to take this opportunity as a challenge and build upon what you’ve learned in this tutorial to implement additional functionality.

If you have any feedback or questions, please don’t hesitate to leave a comment below. I hope you found this article helpful!