Introduction
This guide explains how to build an example web API in Go to create, update, delete book records from a database. At the end of this article, you should be able to create endpoints and understand how routing works with the Fiber framework. You'll also use Gorm for object-relational mapping with PostgreSQL.
Prerequisites
- Have Golang version 1.1x installed on your machine.
- Basic knowledge of Golang.
- Basic knowledge of SQL.
- Have PostgreSQL installed on your PC.
What is Fiber?
Fiber is an Express-inspired web framework built on top of Fasthttp, the fastest HTTP engine for Go. It's "designed to ease things up for fast development with zero memory allocation and performance in mind" according to the Fiber documentation.
Set up the Project
In your terminal, create a Gofiber directory and change to it.
$ mkdir Gofiber $ cd Gofiber
Initialize the project.
$ go mod init github.com/<your GitHub username>/Gofiber
Generate the required folders.
$ mkdir service storage models
Install the Required Libraries
Install Gofiber.
$ go get -u github.com/gofiber/fiber/v2
Install Gorm.
$ go get -u gorm.io/gorm
Install Gorm Postgres driver.
$ go get -u gorm.io/driver/postgres
Install Godotenv, used for loading environment variables.
$ go get github.com/joho/godotenv
Install Validator.
$ go get github.com/go-playground/validator/v10
Create the Database
PostgreSQL creates a default user to access the psql shell.
Switch to the Postgres user and execute a shell.
$ sudo -iu postgres psql
Create the database.
CREATE DATABASE gofiber;
Exit the interactive shell.
\q
Create a
.env
file in your code editor and paste the following to it:DB_HOST=localhost DB_PORT=5432 DB_USER=yourusername DB_PASS=yourpassword DB_NAME=gofiber DB_SSLMODE=disable
Create a file named
postgres.go
and paste the following:package storage import ( "fmt" "gorm.io/driver/postgres" "gorm.io/gorm" ) type Config struct { Host string Port string Password string User string DBName string SSLMode string } func NewConnection(config *Config) (*gorm.DB, error) { dsn := fmt.Sprintf( "host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", config.Host, config.Port, config.User, config.Password, config.DBName, config.SSLMode, ) db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) if err != nil { return db, err } return db, nil }
The main purpose of this code is to connect the code to the database by providing the env values when calling this function in the main function. After you create a function that opens the connection to the database by passing the value from the config struct, which comes in as the argument and will be supplied when calling this function in the main function.
Create the Books Model
Create a file in the models
folder and name it books.go
, then add this to it:
package models
import "gorm.io/gorm"
type Books struct {
ID uint `gorm:"primary key;autoIncrement" json:"id"`
Author *string `json:"author"`
Title *string `json:"title"`
Publisher *string `json:"publisher"`
}
func MigrateBooks(db *gorm.DB) error {
err := db.AutoMigrate(&Books{})
return err
}
Books structs have been created here, and the migration function takes an argument of the database and migrates the table with the AutoMigrate
function that also takes the struct to be migrated.
Service
This is the handler that will pass our request to the DB. We will start by creating a book struct for parsing request body and a repository struct that holds a pointer to the database value.
type Book struct {
Author string `json:"author" validate:"required"`
Title string `json:"title" validate:"required"`
Publisher string `json:"publisher" validate:"required"`
}
type Repository struct {
DB *gorm.DB
}
Here we have created a struct that will be what will be matched when the user sends the request from a body. The validate:required
tag is what will be used to make sure the user does not omit any field when creating the values.
After that step, you will create the handlers, which will be:
- Create books
- Update book by id
- Delete book by id
- Get all books
- Get books by id
Create Books
func (r *Repository) CreateBook(context *fiber.Ctx) error {
book := Book{}
err := context.BodyParser(&book)
if err != nil {
context.Status(http.StatusUnprocessableEntity).JSON(
&fiber.Map{"message": "request failed"})
return err
}
validator := validator.New()
err = validator.Struct(Book{})
if err != nil {
context.Status(http.StatusUnprocessableEntity).JSON(
&fiber.Map{"message": err},
)
return err
}
err = r.DB.Create(&book).Error
if err != nil {
context.Status(http.StatusBadRequest).JSON(
&fiber.Map{"message": "could not create book"})
return err
}
context.Status(http.StatusOK).JSON(&fiber.Map{
"message": "book has been successfully added",
})
return nil
}
What is being done here is creating a new method as a handler that takes the repository struct and an argument of the Fiber context, which will be used in the function for the operations involved and matching the information passed to the book model you have created earlier. You then proceed to check if the user has any empty fields with the validator package and the validator.Struct
, which takes the struct to be validated and returns an error. After that, you check the error and send back a message to the user with the Fiber method that accepts a request status and send a JSON with the error as a message. What comes after this is inserting the values provided in the database with the gorm
function Create
, which returns an error, and it is treated the same way the validator error is treated. If there is no error, you pass a message to the user that the book has been added to the database has been added successfully.
Update Books by ID
func (r *Repository) UpdateBook(context *fiber.Ctx) error {
id := context.Params("id")
if id == "" {
context.Status(http.StatusInternalServerError).JSON(&fiber.Map{
"message": "id cannot be empty",
})
return nil
}
bookModel := &models.Books{}
book := Book{}
err := context.BodyParser(&book)
if err != nil {
context.Status(http.StatusUnprocessableEntity).JSON(
&fiber.Map{"message": "request failed"})
return err
}
err = r.DB.Model(bookModel).Where("id = ?", id).Updates(book).Error
if err != nil {
context.Status(http.StatusBadRequest).JSON(&fiber.Map{
"message": "could not update book",
})
return err
}
context.Status(http.StatusOK).JSON(&fiber.Map{
"message": "book has been successfully updated",
})
return nil
}
What is being done here is getting the id with the Fiber function called param
, which accepts an argument which is the name of the parameter expecting. If the id is not present, an error is being sent to the user with the Fiber function. What comes after that is matching the information passed to the book model with the BodyParser
as you have done in the create books method. After that, you will use the Gorm function where
to find where the id matches and update the whole record with the updates
function and handle errors appropriately.
Delete Book by ID
func (r *Repository) DeleteBook(context *fiber.Ctx) error {
bookModel := &models.Books{}
id := context.Params("id")
if id == "" {
context.Status(http.StatusInternalServerError).JSON(&fiber.Map{
"message": "id cannot be empty",
})
return nil
}
err := r.DB.Delete(bookModel, id)
if err.Error != nil {
context.Status(http.StatusBadRequest).JSON(&fiber.Map{
"message": "could not delete book",
})
return err.Error
}
context.Status(http.StatusOK).JSON(&fiber.Map{
"message": "book has been successfully deleted",
})
return nil
This process is similar to updating the book by its id but the difference is using the Delete method to delete the record.
Get All Books
func (r *Repository) GetBooks(context *fiber.Ctx) error {
bookModels := &[]models.Books{}
err := r.DB.Find(bookModels).Error
if err != nil {
context.Status(http.StatusBadRequest).JSON(
&fiber.Map{"message": "could not get books"})
return err
}
context.Status(http.StatusOK).JSON(&fiber.Map{
"message": "books gotten successfully",
"data": bookModels,
})
return nil
}
This process involves creating a slice of the book model before using the find
method to get all the records and send them back to the user.
Get Books by ID
func (r *Repository) GetBookByID(context *fiber.Ctx) error {
id := context.Params("id")
bookModel := &models.Books{}
if id == "" {
context.Status(http.StatusInternalServerError).JSON(&fiber.Map{
"message": "id cannot be empty",
})
return nil
}
err := r.DB.Where("id = ?", id).First(bookModel).Error
if err != nil {
context.Status(http.StatusBadRequest).JSON(
&fiber.Map{"message": "could not get book"})
return err
}
context.Status(http.StatusOK).JSON(&fiber.Map{
"message": "books id gotten successfully",
"data": bookModel,
})
return nil
}
This process is also similar to updating by the id record, but this time, we are getting the record by the id using the first
method, which gets the record that matches the id first on the table and accepts the argument of the model to match it to, you check for the error, and you handle it as it is being done above if there is no error you will send a message and data back to the user.
Set up the Routes
func (r *Repository) SetupRoutes(app *fiber.App) {
api := app.Group("/api")
api.Post("/create_books", r.CreateBook)
api.Delete("/delete_book/:id", r.DeleteBook)
api.Put("/update_book/:id", r.UpdateBook)
api.Get("/get_books/:id", r.GetBookByID)
api.Get("/books", r.GetBooks)
}
What is happening here is creating a function with the repository that will set up our routes for the handlers. Here the app.Group
accepts a string that will act as the parent route for the rest of the routes, and all other ones come after it e.g. localhost:8080/api/books
for calling the GetBooks
function that gets all the created books in the database.
Set up the Main Function
func main() {
err := godotenv.Load(".env")
if err != nil {
log.Fatal(err)
}
config := &storage.Config{
Host: os.Getenv("DB_HOST"),
Port: os.Getenv("DB_PORT"),
Password: os.Getenv("DB_PASS"),
User: os.Getenv("DB_USER"),
SSLMode: os.Getenv("DB_SSLMODE"),
DBName: os.Getenv("DB_NAME"),
}
db, err := storage.NewConnection(config)
if err != nil {
log.Fatal("could not load database")
}
err = models.MigrateBooks(db)
if err != nil {
log.Fatal("could not migrate db")
}
r := &service.Repository{
DB: db,
}
app := fiber.New()
r.SetupRoutes(app)
app.Listen(":8080")
}
What comes first here is loading the .env
file and filling the storage config struct with the values from the env file, that you will create the connection and migrate the database. After that comes the filling of the repository with the connected DB and then setting up the routes by passing the initialized Fiber function as the argument value in the SetupRoute
function. Finally, you will start the app with the Listen
function, which accepts an argument of the port number desired to run the app.
Conclusion
Now you should be able to freely use the Fiber framework to get, post, update, and delete requests and use the Gorm library to connect PostgreSQL to the code.