Build REST API with Go Fiber and PlanetScale

7 MIN READ

Introduction

Fiber (gofiber.io) is web framework written in Go that much like Express in Nodejs. Fiber have many built in features to build rich web application such as Middleware, API Ready, Template Engine, Websocket support, Rate limiter, etc. They used fasthttp for the engine that claims is the fastest HTTP Engine in Go.

Today, we are going to build a simple REST API with Fiber and integrated with PlanetScale for the database.

PlanetScale is serverless database platform for developers that built on top Vitess.

Set up PlanetScale

Go to https://planetscale.com/ and sign up, then setup your account such as organization, etc. Then, we'll create database via PlanetScale CLI, you can also create database via web.

I don't want to explain how to setup the CLI, PlanetScale's team has managed to explain it very clear. Check out the documentation PlanetScale CLI

Once success to setup the CLI, then create database called fiber-pscale and specify the region of your database. I recommended to using the region that close that where you are. In my case I would like to use Asia Pacific Singapore. You can check the list of region, there are 6 regions at the moment.

$ pscale region list
  NAME (6)                 SLUG           ENABLED
 ------------------------ -------------- ---------
  US East                  us-east        Yes
  US West                  us-west        Yes
  EU West                  eu-west        Yes
  Asia Pacific Mumbai      ap-south       Yes
  Asia Pacific Singapore   ap-southeast   Yes
  Asia Pacific Tokyo       ap-northeast   Yes

Create database

$ pscale database create fiber-pscale --region ap-southeast
Database fiber-pscale was successfully created.

Set up Application

We are going to use go modules, first we create empty directory and then initialize the modules. You can use your repo url or application name. In this case, I'm using my repo url.

mkdir fiber-pscale && cd fiber-pscale

go mod init github.com/maful/fiber-pscale

Then, there are 3 packages we're going to build. Package means the directory. Create thress directories.

  • cmd : this is the root application for initialize the Fiber and connect to PlanetScale database
  • handler : contains all handler for the routing such as list data, create, etc
  • models : contains data structure for the application
mkdir cmd
mkdir handlers
mkdir models

Since we are going to use Fiber and PlanetScale, install the modules

To access PlanetScale, we can use ORM from Go called Gorm, and since PlabetScale database is built on top Vitess (MySQL), install the MySQL driver as well.

# install fiber
$ go get -u github.com/gofiber/fiber/v2

# install gorm to connect to planetscale database
$ go get -u gorm.io/gorm

# mysql driver
$ go get -u gorm.io/driver/mysql

Basic Handler

write basic handler with Fiber, create file called main.go inside cmd directory and add this script

package main

import (
    "net/http"

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

func main() {
    app := fiber.New(fiber.Config{
        AppName:      "Fiber with Planetscale",
        ServerHeader: "Fiber",
    })

    app.Use(logger.New())

    app.Get("/", func(c *fiber.Ctx) error {
        return c.Status(http.StatusOK).JSON(&fiber.Map{
            "message": "Hello world",
        })
    })

    app.Listen(":3000")
}

it will create a new instance of fiber server, and listen to PORT 3000, back to terminal and run the main.go

$ go run cmd/main.go

 ┌───────────────────────────────────────────────────┐
 │              Fiber with Planetscale               │
 │                   Fiber v2.18.0                   │
 │               http://127.0.0.1:3000               │
 │       (bound on host 0.0.0.0 and port 3000)       │
 │                                                   │
 │ Handlers ............. 3  Processes ........... 1 │
 │ Prefork ....... Disabled  PID ............. 22443 │
 └───────────────────────────────────────────────────┘

and then you can access it at http://localhost:3000, below is an example with curl command

$ curl --location --request GET 'http://localhost:3000'
{"message":"Hello world"}

you could use Postman or other similar software if prefer to use GUI.

Models

Create User model, create file called user.go inside models directory, and define a struct with gorm

package models

import "gorm.io/gorm"

// User struct
type User struct {
    gorm.Model
    Name    string `json:"name"`
    Email   string `json:"email"`
    Website string `json:"website"`
}

Connect to Database

Create development database branch called add-users-table

$ pscale branch create fiber-pscale add-users-table

open new terminal tab, we're going to connect to database inside add-users-table branch and listen to 3309 PORT. See more Connect using client certificates

$ pscale connect fiber-pscale add-users-table --port 3309

Create file called database.go inside models directory and add function to connect to database

package models

import (
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

var DB *gorm.DB

func ConnectDatabase() {
    // refer https://github.com/go-sql-driver/mysql#dsn-data-source-name for details
    dsn := "root:@tcp(127.0.0.1:3309)/fiber-pscale?charset=utf8mb4&parseTime=True&loc=Local"
    database, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        panic("failed to connect database")
    }

    // Migrate the users table
    database.AutoMigrate(&User{})

    DB = database
}

open your main.go, and call the ConnectDatabase function to migrate the tables and connect to database.

import (
    // ...

    "github.com/maful/fiber-pscale/models"
)

func main() {
    r := gin.Default()

    models.ConnectDatabase() // New

    // ....
}

then run the app go run cmd/main.go, Gorm will automatically migrate the table into add-users-table branch. How to know if the migration is success? You can check in Planetscale dashboard for the following branch, or using CLI to see the schema for add-users-table branch.

$ pscale branch schema fiber-pscale add-users-table
-- users --
CREATE TABLE `users` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `created_at` datetime(3) DEFAULT NULL,
  `updated_at` datetime(3) DEFAULT NULL,
  `deleted_at` datetime(3) DEFAULT NULL,
  `name` longtext,
  `email` longtext,
  `website` longtext,
  PRIMARY KEY (`id`),
  KEY `idx_users_deleted_at` (`deleted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

this schema won't apply to main branch until you create a deploy request

lastly, stop the connection from add-users-table branch or Ctrl+C.

Deploy the schema

now, deploy the add-users-table branch into main branch

$ pscale deploy-request create fiber-pscale add-users-table
Deploy request #1 successfully created.

check your dashboard, you'll see here that you requested schema changes from development branch to main branch. If you are familiar with Git, it kind of Pull Request.

01

now, approve the request and the PlanetScale automatically apply the new schema into main branch without downtime

$ pscale deploy-request deploy fiber-pscale 1
Successfully deployed dhacze78ukhv from add-users-table to main.

now, back and check the dashboard, it's deployed 🎊

02

if you no longer need to make schema changes from the branch, now you can safely delete it

$ pscale branch delete fiber-pscale add-users-table

Handlers

create a file called users.go inside handlers directory.

now, create a connection to main branch so the application can communicate with the database.

$ pscale connect fiber-pscale main --port 3309

Note: It's recommended to use secure connection while connecting to main branch once you deployed your application. More https://docs.planetscale.com/reference/secure-connections

Create a user

Create a function CreateUser and initialize createUserRequest struct in users.go

package handlers

import (
    "net/http"

    "github.com/gofiber/fiber/v2"
    "github.com/maful/fiber-pscale/models"
)

type createUserRequest struct {
    Name    string `json:"name" binding:"required"`
    Email   string `json:"email" binding:"required"`
    Website string `json:"website" binding:"required"`
}

func CreateUser(c *fiber.Ctx) error {
    req := &createUserRequest{}
    if err := c.BodyParser(req); err != nil {
        return c.Status(http.StatusBadRequest).JSON(&fiber.Map{
            "message": err.Error(),
        })
    }

    user := models.User{
        Name:    req.Name,
        Email:   req.Email,
        Website: req.Website,
    }
    models.DB.Create(&user)

    return c.Status(fiber.StatusCreated).JSON(&fiber.Map{
        "user": user,
    })
}

open main.go, add a new handler to call that function

import (
    // ...
    "github.com/maful/fiber-pscale/handlers"
)

// app.Get("/" ...
app.Post("/users", handlers.CreateUser)

run the app, and try to create the user

$ curl --location --request POST 'http://localhost:3000/users' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name": "John",
    "email": "john@example.com",
    "website": "https://example.com"
}'

response

{
    "user": {
        "ID": 1,
        "CreatedAt": "2021-09-06T20:04:31.022+07:00",
        "UpdatedAt": "2021-09-06T20:04:31.022+07:00",
        "DeletedAt": null,
        "name": "John",
        "email": "john@example.com",
        "website": "https://example.com"
    }
}

List all users

create a new function called GetUsers below the create function

func GetUsers(c *fiber.Ctx) error {
    var users []models.User
    models.DB.Find(&users)

    return c.Status(http.StatusOK).JSON(&fiber.Map{
        "users": users,
    })
}

register the function to the app, place above the create user handler

app.Get("/users", handlers.GetUsers)
// app.Post("/users...

stop the existing app, and run again. You should always do this because the app doesn't refresh automatically when we made a changes.

curl --location --request GET 'http://localhost:3000/users'

response

{
    "users": [
        {
            "ID": 1,
            "CreatedAt": "2021-09-06T20:04:31.022+07:00",
            "UpdatedAt": "2021-09-06T20:04:31.022+07:00",
            "DeletedAt": null,
            "name": "John",
            "email": "john@example.com",
            "website": "https://example.com"
        }
    ]
}

View user

add a new function called GetUser in the bottom

// ...

func GetUser(c *fiber.Ctx) error {
    var user models.User
    if err := models.DB.First(&user, "id = ?", c.Params("id")).Error; err != nil {
        return c.Status(http.StatusNotFound).JSON(&fiber.Map{
            "message": "Record not found!",
        })
    }

    return c.Status(http.StatusOK).JSON(&fiber.Map{
        "user": user,
    })
}

and add new handler in main.go

// ...
app.Get("/users", handlers.GetUsers)
app.Get("/users/:id", handlers.GetUser) // new

now, we try to get the user details with id 1

curl --location --request GET 'http://localhost:3000/users/1'

response

{
    "user": {
        "ID": 1,
        "CreatedAt": "2021-09-06T20:04:31.022+07:00",
        "UpdatedAt": "2021-09-06T20:04:31.022+07:00",
        "DeletedAt": null,
        "name": "John",
        "email": "john@example.com",
        "website": "https://example.com"
    }
}

if try to get the id that doesn't exist

curl --location --request GET 'http://localhost:3000/users/100'
{
    "message": "Record not found!"
}

Update a user

again, add a new function called UpdateUser in the users handler

// ...

func UpdateUser(c *fiber.Ctx) error {
    // first, check if the user is exist
    user := models.User{}
    if err := models.DB.First(&user, "id = ?", c.Params("id")).Error; err != nil {
        return c.Status(http.StatusNotFound).JSON(&fiber.Map{
            "message": "Record not found!",
        })
    }

    // second, parse the request body
    request := &updateUserRequest{}
    if err := c.BodyParser(request); err != nil {
        return c.Status(http.StatusBadRequest).JSON(&fiber.Map{
            "message": err.Error(),
        })
    }

    // third, update the user
    updateUser := models.User{
        Name:    request.Name,
        Email:   request.Email,
        Website: request.Website,
    }
    models.DB.Model(&user).Updates(&updateUser)

    return c.Status(http.StatusOK).JSON(&fiber.Map{
        "user": user,
    })
}

register update user to main.go

app.Put("/users/:id", handlers.UpdateUser)

now, re-run the application

update the user that we created before

$ curl --location --request PUT 'http://localhost:3000/users/1' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name": "Machine name"
}'

response

{
    "user": {
        "ID": 1,
        "CreatedAt": "2021-09-08T08:07:25.042+07:00",
        "UpdatedAt": "2021-09-08T08:15:52.249+07:00",
        "DeletedAt": null,
        "name": "Machine name",
        "email": "joh@example.com",
        "website": "google.com"
    }
}

when the user doesn't exist

$ curl --location --request PUT 'http://localhost:3000/users/100' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name": "Machine name"
}'

response

{
    "message": "Record not found!"
}

Delete a user

Add delete user function in the bottom of users handler.

func DeleteUser(c *fiber.Ctx) error {
    // first, check if the user is exist
    user := models.User{}
    if err := models.DB.First(&user, "id = ?", c.Params("id")).Error; err != nil {
        return c.Status(http.StatusNotFound).JSON(&fiber.Map{
            "message": "Record not found!",
        })
    }

    // second, delete the user
    models.DB.Delete(&user)

    return c.Status(http.StatusOK).JSON(&fiber.Map{
        "message": "Success",
    })
}

register the function

app.Delete("/users/:id", handlers.DeleteUser)

so, create a new user again

$ curl --location --request POST 'http://localhost:3000/users' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name": "Badu",
    "email": "joh@example.com",
    "website": "google.com"
}'

see the id from the response, we will delete that user

$ curl --location --request DELETE 'http://localhost:3000/users/2'

response

{
    "message": "Success"
}

Summary

PlanetScale offer Developer plan pricing that you can use for the development lifecycle and it's completely FREE. You can create up to 3 databases and 3 branches each database. Basically, this is going to be a new knowledge for the developer who never use serverless database and with new workflow how to make a schema.

Fiber is a great web framework to build application in Go, they are fast, rich features and the documentation is good.

In this post, it's just simple web api application to give basic understanding how to use Fiber and PlanetScale database. In the next one, we are going to build more complex web api with the same tech stacks.

Download the full source code on this repository.

Thank you.