Build REST API with Go Fiber and PlanetScale
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.
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 🎊
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.