Taking Orders
We start building our Café by developing the Servitør API that will accept orders and putting them in the MySQL database.
The Servitør will be developed in Go using the Gin Web Framework and the GORM library.
Getting started
Start the project with
go mod init schildcafe.servitor
and install the gin framework
go get github.com/gin-gonic/gin
Now it’s time for the first code
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
func setupRouter() *gin.Engine {
r := gin.New()
r.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "Welcome to the SchildCafé!")
})
return r
}
func main() {
fmt.Println("vim-go")
r := setupRouter()
r.Run(":1333")
}
Try running it
go run .
and check
curl http://localhost:1333
Healthcheck
Implement the /healthcheck endpoint.
Solution
Inside the setupRouter() function, after r := gin.New() and before the return r, add
r.GET("/healthcheck", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Ok"})
})
As per specs, the port must be configurable, so let’s check for an overwrite value from the environment variables.
We need to import the "os" module and then define
func getEnv(name string, defaultValue string) string {
value, exists := os.LookupEnv(name)
if exists {
return value
}
return defaultValue
}
and then we have
runPort := getEnv("SERVITOR_PORT", "1333")
r := setupRouter()
r.Run(":" + runPort)
Test it out
Try a serving from a different port.
Modelling the orders
Let’s install the gorm library
go get gorm.io/gorm
go get gorm.io/driver/mysql
We’ll be having orders of the form
type order struct {
ID string `json:"orderId" gorm:"type:varchar(50);primaryKey"`
OrderReceived time.Time `json:"orderReceived"`
OrderReady time.Time `json:"orderReady" gorm:"default:null"`
OrderRetrieved time.Time `json:"orderRetrieved" gorm:"default:null"`
OrderSize int `json:"orderSize"`
OrderBrewed int `json:"orderBrewed"`
}
Note that the ID is a string, as we are using UUIDs. Thus we’ll need a uuid library
go get github.com/gofrs/uuid
We also need to store the individual coffee jobs
type coffeeListItem struct {
ID string `json:"jobID" gorm:"type:varchar(50);primary_key"`
Product string `json:"coffeeProduct"`
OrderID string `json:"orderId"`
Order order
OrderReceived time.Time `json:"orderReceived"`
Machine string `json:"machine" gorm:"default:null"`
JobStarted time.Time `json:"jobStarted" gorm:"default:null"`
JobReady time.Time `json:"jobReady" gorm:"default:null"`
JobRetrieved time.Time `json:"jobRetrieved" gorm:"default:null"`
}
Now, in order to talk to the database, we need to establish a connection.
Let’s import "gorm.io/gorm" and "gorm.io/driver/mysql" and add the following to the start of main()
dsn := getEnv("MYSQL_USER", "root") + ":" + getEnv("MYSQL_PASS", "root") +
"@tcp(" + getEnv("MYSQL_HOST", "localhost") + ":" + getEnv("MYSQL_PORT", "3306") + ")/" +
getEnv("MYSQL_DB", "cafe") + "?charset=utf8mb4&parseTime=True&loc=Local"
db, dbErr := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if dbErr != nil {
panic("failed to connect database")
}
db.Debug().AutoMigrate(&coffeeListItem{})
db.Debug().AutoMigrate(&order{})
The last two lines will ensure the tables exist, but we’ll assume the database is created externally.
We’ll be running mysql in a docker container for the purpose of this exercise.
docker run --name=mysqlserver -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -d mysql:5.6
Don’t for get to delete the container later:
docker stop mysqlserver && docker rm mysqlserver
Exercise
Create the database.
Solution
docker exec -it mysqlserver mysql -uroot -proot
and execute
CREATE DATABASE cafe;
Receiving orders
Let’s model the order payload
type orderSubmission struct {
ID string `json:"orderId" gorm:"primaryKey"`
Coffees []orderEntry `json:"coffeeOrder"`
}
type orderEntry struct {
Product string `json:"product"`
Count int `json:"count"`
}
The implementation of the endpoint is part of our setupRouter() function.
r.POST("/submit-order", func(c *gin.Context) {
var incomingOrder orderSubmission
c.BindJSON(&incomingOrder)
resultID := newOrder(incomingOrder.Coffees)
fmt.Println("Order " + resultID + " accepted")
c.JSON(http.StatusOK, resultID)
})
While the actual work will be done by
func newOrder(orderedCoffees []orderEntry) string {
}
Let’s build this function one step at a time. We first need to create the order
var newOrder order
myOrderIDUUID, _ := uuid.NewV4()
newOrder.ID = myOrderIDUUID.String()
newOrder.OrderReceived = time.Now().UTC()
We also need to store it’s size for later use
var newOrderSize int = 0
for _, item := range orderedCoffees {
newOrderSize += item.Count
}
newOrder.OrderSize = newOrderSize
and that’s the order done.
db.Create(&newOrder)
For the individual jobs, we just loop over the items and their counts.
for _, item := range orderedCoffees {
for i := 0; i < item.Count; i++ {
var newCoffee coffeeListItem
myCoffeeIDUUID, _ := uuid.NewV4()
newCoffee.ID = myCoffeeIDUUID.String()
newCoffee.OrderID = newOrder.ID
newCoffee.Product = item.Product
newCoffee.OrderReceived = newOrder.OrderReceived
db.Create(&newCoffee)
fmt.Println("New Job: " + newCoffee.Product + " with ID: " + newCoffee.ID +
", part of order " + newCoffee.OrderID)
}
}
Finally, let’s return the order’s UUID as that’s needed later
return newOrder.ID
Exercise
Refactor the code so as not to loop over orderedCoffees twice.