Build a REST API with Golang from scratch: PostgreSQL with Gorm and Gin Web Framework

Photo by Jeroen den Otter on Unsplash

What is the best programming language in 2020?

Let’s take a look

Go is popular in Cloud area and used by some of the biggest Cloud Native project like Kubernetes and Docker. You may hear those fancy features such as concurrency, high performance and simplicity provided by Go.

Today I want to share my 2 cents that how to build a REST API with Go.

Prerequisites

  • Go
  • Docker and docker compose

If you are using Mac OS, you may run brew install go

You may choose to download the binary release and install it.

Run command below to check the version. I am using go1.14 for this project.

$ go version 
go1.14 darwin/amd64

Install Docker and Compose on macOS

As docker official website state:

Docker Desktop for Mac and Docker Toolbox already include Compose along with other Docker apps, so Mac users do not need to install Compose separately. Docker install instructions for these are here:

Get the Docker Desktop for Mac here and install it.

Go has a 3 ways to build a project.

method 1. Using Go Path

By using Go path, you will need to your project put in a single workspace. You need to set GOPATH environment variable in your system.

method 2. Using package manager(<go1.13)

You may use package manager to help you to fetch all the dependency in your project, such as Dep and Glide. There is a list of tools you can use from https://github.com/golang/go/wiki/packagemanagementtools

method 3. Using Go Mod (Recommend, latest)

Go modules is the new (official) way to manage package dependencies.

During the time I writing this project, I suffer from similar error message when using method 1 and 2. That is why I recommend this method as it is official solution too.

[ERROR]	Failed to retrieve a list of dependencies[ERROR] cant't get golang package 

[ERROR] cannot find package "."

If you are using go1.13, you need to flag GO111MODULE in your terminal

export GO111MODULE=on

You may check the link below for more information about Go Mod

Getting Started

You can get the source code here

This project have the following structure:

├── README.md
├── controllers
│ └── book.go (Rest api CRUD action)
├── database.env (environment variable)
├── go.mod (generated by go mod)
├── go.sum (generated by go mod)
├── main.go (project entry)
├── models
│ ├── book.go (book class structure)
│ └── setup.go (setup database)
├── Dockerfile
├── docker-compose.yaml

You can start from empty folder run command

$ go mod init

which help us to manage the dependencies that are specifically installed for this project. Create the files and copy and paste the following content.

Models/book.go

package modelstype Book struct {
ID uint `json:"id" gorm:"primary_key"`
Title string `json:"title"`
Author string `json:"author"`
}
type CreateBookInput struct {
Title string `json:"title" binding:"required"`
Author string `json:"author" binding:"required"`
}
type UpdateBookInput struct {
Title string `json:"title"`
Author string `json:"author"`
}

We setup a book model, which is the data structure of book and how we want to store it with database

Models/setup.go

package modelsimport (
"fmt"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres" // using postgres sql
"github.com/spf13/viper"
)
func SetupModels() *gorm.DB {//db, err := gorm.Open("sqlite3", "test.db")// Enable VIPER to read Environment Variables
viper.AutomaticEnv()
// To get the value from the config file using key// viper package read .env
viper_user := viper.Get("POSTGRES_USER")
viper_password := viper.Get("POSTGRES_PASSWORD")
viper_db := viper.Get("POSTGRES_DB")
viper_host := viper.Get("POSTGRES_HOST")
viper_port := viper.Get("POSTGRES_PORT")
// https://gobyexample.com/string-formatting
prosgret_conname := fmt.Sprintf("host=%v port=%v user=%v dbname=%v password=%v sslmode=disable", viper_host, viper_port, viper_user, viper_db, viper_password)
fmt.Println("conname is\t\t", prosgret_conname)db, err := gorm.Open("postgres", prosgret_conname)
if err != nil {
panic("Failed to connect to database!")
}
db.AutoMigrate(&Book{})// Initialise value
m := Book{Author: "author1", Title: "title1"}
db.Create(&m)return db
}

In this setup.go, we using

1. GORM for the POSTGRES SQL adapter.

2. Viper for fetching environment variable

3. fmt to print output into console

Lastly, form a connection string for GORM to connection to PostgresSQL and initialize value in database.

Take note there is a _ in front of github.com/jinzhu/gorm/dialects/postgres

The purpose of _ is for import for side effect, without any explicit use. Read more from https://golang.org/doc/effective_go.html#blank

Controllers/book.go

package controllersimport (
models "golang-example/models"
"net/http"
"github.com/gin-gonic/gin"
"github.com/jinzhu/gorm"
)
// GET /books
// Get all books
func FindBooks(c *gin.Context) {
db := c.MustGet("db").(*gorm.DB)
var books []models.Book
db.Find(&books)
c.JSON(http.StatusOK, gin.H{"data": books})
}
// POST /books
// Create new books
func CreateBook(c *gin.Context) {
db := c.MustGet("db").(*gorm.DB)
// Validate input
var input models.CreateBookInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Create Book
book := models.Book{Title: input.Author, Author: input.Author}
db.Create(&book)
c.JSON(http.StatusOK, gin.H{"data": book})}// GET /books/:id
// Find a book
func FindBook(c *gin.Context) {
db := c.MustGet("db").(*gorm.DB)
// Get model if exist
var book models.Book
if err := db.Where("id = ?", c.Param("id")).First(&book).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
return
}
c.JSON(http.StatusOK, gin.H{"data": book})
}
// PATCH /books/:id
// Update a book
func UpdateBook(c *gin.Context) {
db := c.MustGet("db").(*gorm.DB)
// Get model if exist
var book models.Book
if err := db.Where("id = ?", c.Param("id")).First(&book).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
return
}
// Validate input
var input models.UpdateBookInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
db.Model(&book).Updates(input)c.JSON(http.StatusOK, gin.H{"data": book})
}
// DELETE /books/:id
// Delete a book
func DeleteBook(c *gin.Context) {
db := c.MustGet("db").(*gorm.DB)
// Get model if exist
var book models.Book
if err := db.Where("id = ?", c.Param("id")).First(&book).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
return
}
db.Delete(&book)c.JSON(http.StatusOK, gin.H{"data": true})
}

we separate the business logic and place it into Controller/book.go

Create 4 functions:

  1. FindBooks (get All Book)
  2. CreateBook (create a book, id auto incremental)
  3. FindBook (find a book by id)
  4. UpdateBook (update a book by id)
  5. DeleteBook (delete a book by id)

Main.go

package mainimport (
controllers "golang-example/controllers" // new
"golang-example/models" // new
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
db := models.SetupModels() // new// Provide db variable to controllers
r.Use(func(c *gin.Context) {
c.Set("db", db)
c.Next()
})
r.GET("/books", controllers.FindBooks)r.POST("/books", controllers.CreateBook) // creater.GET("/books/:id", controllers.FindBook) // find by idr.PATCH("/books/:id", controllers.UpdateBook) // update by idr.DELETE("/books/:id", controllers.DeleteBook) // delete by idr.Run()
}

Lastly, we use Gin to expose those functions created in Controller into REST API

Start the program

We will using docker and docker-compose to ease the testing stage. The benefit is you do not need to install PostgresSQL in your local environment. Instead, let docker help you to manage. Create the files and copy and paste the following content.

Dockerfile

# https://blog.golang.org/docker
# Start from a Debian image with the latest version of Go installed
# and a workspace (GOPATH) configured at /go.
# FROM s390x/golang:1.14-alpine
############################
# STEP 1 build executable binary
############################
# golang alpine 1.13.5
FROM golang@sha256:0991060a1447cf648bab7f6bb60335d1243930e38420bee8fec3db1267b84cfa as builder
ENV GO111MODULE=on# Install git + SSL ca certificates.
# Git is required for fetching the dependencies.
# Ca-certificates is required to call HTTPS endpoints.
# Else you will get error => local error: tls: bad record MAC
RUN apk update && apk add --no-cache git ca-certificates tzdata && update-ca-certificates
# Create appuser
ENV USER=appuser
ENV UID=10001
# Install git + SSL ca certificates.
# Git is required for fetching the dependencies.
# Ca-certificates is required to call HTTPS endpoints.
# RUN apk update && apk add --no-cache git ca-certificates tzdata && update-ca-certificates
# See https://stackoverflow.com/a/55757473/12429735
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
"${USER}"
WORKDIR $GOPATH/src/mypackage/myapp/
COPY . .
# Fix the error cannot find package "golang.org/x/net/html" in any of:
# default: #10 0.286 /usr/local/go/src/golang.org/x/net/html (from $GOROOT)
# default: #10 0.286 /go/src/golang.org/x/net/html (from $GOPATH)
RUN go mod vendor
RUN ls# Build the binary
# RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /go/bin/hello main.go
# -mod vendor
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /go/bin/hello -mod vendor main.go
############################
# STEP 2 build a small image
############################
FROM scratch
# Import from builder.
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group
# Copy our static executable
COPY --from=builder /go/bin/hello /go/bin/hello
# Use an unprivileged user.
USER appuser:appuser
# Run the hello binary.
ENTRYPOINT ["/go/bin/hello"]
# Run the outyet command by default when the container starts.
# ENTRYPOINT /go/bin/outyet
# Document that the service listens on port 8080.
EXPOSE 8080

docker-compose.yaml

version: '3.7'volumes:
database_data:
driver: local
services:
db:
image: 'postgres:latest' # use latest official postgres version
ports:
- '5432:5432'
expose:
- 5432
environment:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: example
volumes:
- database_data:/var/lib/postgresql/data
web:
build: .
ports:
- '8080:8080'
environment:
POSTGRES_HOST: db
POSTGRES_PORT: 5432
env_file:
- database.env # configure postgres
links:
- dbValidation

Run the command

$ docker-compose up

Wait for it to build and to see following message

docker-compose message

Validation

Open new terminal and run command. You will see the data return according the model that you define above.

$ curl http://localhost:8080/books                                                      
{"data":[{"id":1,"title":"title1","author":"author1"}]}

You can use IBM API Connect Test and Monitor to post or delete a book record in the PostgresSQL database.

Post to create a new book

Summary

Writing Golang with Gin can be easy and fast. As we utilise the Gorm to help us to connect to database.

Gin might not be suitable for large backend applications as it is minimalistic framework. See the comparison below.

If you interested on Golang and wish to become a golang developer, check the guide below.

Inspired by

https://blog.logrocket.com/how-to-build-a-rest-api-with-golang-using-gin-and-gorm

That’s all! See you next time!

--

--

--

https://www.youracclaim.com/users/jia-hao-chu/

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Term 1 Finished. Enter Term 2.

Extracting And Analysing Spotify Tracks With Python

How to choose between REST & GraphQL?

“Post Corona Era, Blockchain Activation Requires Awareness Conversion” — Blockchain & AI Summit…

What if.

Monitoring sensor data in an Oracle JET Mobile App over WebSocket (Part 2 of 2)

How to print number with commas as thousands separators in Python.

gRPC call on iOS with Swift

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Daniel Chu

Daniel Chu

https://www.youracclaim.com/users/jia-hao-chu/

More from Medium

Build simple gRPC server with Golang

[GoLang] Go run or go build: no such file or directory

GoFrame 101: Add prometheus middleware

Calling a containerized Golang function using RPC

An image depicting how RPC works