setting up Go and initial files
Golang can be installed through the official website. I will be using Go version 1.19.
Once Go is installed, we can set up our backend project structure.
Create our backend folder and initialize our backend workspace with go mod init
. This helps to track our Go dependencies.
mkdir backend
cd backend
# module path can be anything but best is to be the same path as repo
go mod init <module path>
creating the gin engine
Gin is a web framework that we can create API routes for our front end to interface with. To start, we can first import the module into our Go project.
go get -u github.com/gin-gonic/gin
Then inside our router.go
file, create a InitRouter
function to initialize our router instance as r
.
The router object r
is then used to define various HTTP routes for the API. It takes two arguments, the first argument is the HTTP route in string and the second argument is the handler function that will perform a specific action to process the HTTP request. For example, r.GET("/", getServerStatus)
maps a GET request to the root URL path to a handler function called getServerStatus
.
HTTP methods used here include GET, POST, and DELETE. GET requests are commonly used to retrieve information from the server and are considered safe, as multiple calls to the server do not change its state. Therefore, HTTP GET requests are considered idempotent. In contrast, HTTP POST requests are used to create or update data on the server, potentially altering the server's state and making them non-idempotent. We should keep in mind this when implementing our handler function to ensure this holds and to use each HTTP method appropriately depending on the nature of the request.
The summary of the code in the router.go
file is shown below:
// router.go
package routes
import (
"github.com/gin-gonic/gin"
)
func InitRouter() *gin.Engine {
r := gin.Default()
r.GET("/", getServerStatus)
r.POST("/create-room", createRoom)
r.GET("/rooms", getRooms)
return r
}
Overall, this code sets up a web server that listens for HTTP requests and routes them to the appropriate handler functions to process the requests and return responses.
In our main.go
file, we can then call our InitRouter
function and then start the server by calling r.run()
. Using a function with names that start with an uppercase letter will cause it to be visible to other packages in your workspace. A function starting with a lowercase letter can only be used within its own package and cannot be called from another package.
// main.go
package main
import (
// path to my routes folder
"github.com/AndreWongZH/iothome/routes"
)
func main() {
r := routes.InitRouter()
r.Run(":3001")
}
creating handler functions
Let's first implement the HTTP /
route's handler function so that we can query if our server is online. All the handler functions take *gin.Content
as a parameter. Calling ctx.JSON
here will send a JSON response back to the client. If our backend server is online, we will receive a JSON response of {"status": "ok"}
back.
// routes.go
package routes
func getServerStatus(ctx *gin.Context) {
ctx.JSON(http.StatusOK, gin.H{
"status": "ok",
})
}
We will then create two other handlers, createRoom
and getRooms
that will update our internal list rooms
holding all the room names. For our POST route to /create-room
, we want to pass the name of our room as a JSON payload in our HTTP request. We will then create a RoomJSON
struct which can hold a collection of fields. Our RoomJSON
currently has a field called Name
that is of type string. The struct tag is denoted by the two backticks and within that contains json:"name"
. This helps to control the encoding of JSON and will use the lowercase version of name
rather than the field Name
. We can then call ctx.BindJSON()
, which will bind the JSON payload to our RoomJSON struct. If our operation is successful, we can then append the room name to our list of room names.
Lastly, create our getRooms
function that will return our list of rooms as JSON data and can see the rooms that we have added previously.
// routes.go
// package routes
// hold our list of room names
var rooms []string;
// struct to hold JSON body data
type RoomJSON struct {
Name string `json:"name"`
}
func createRoom(ctx *gin.Context) {
var roomJSON RoomJSON
err := ctx.BindJSON(&roomJSON)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"error": "failed to parse JSON data",
})
}
rooms = append(rooms, roomJSON.Name)
ctx.JSON(http.StatusOK, gin.H{
"status": "room name successfully added",
})
}
func getRooms(ctx *gin.Context) {
ctx.JSON(http.StatusOK, gin.H{
"rooms": rooms,
})
}
running and testing our server
we can run our backend server by running
go run main.go
and the sever will be setup as shown below
To test if our API endpoints are working, we can make an HTTP request to the server either on the command line or an API platform such as Postman.
Using command line
# test if server is online
$ curl http://localhost:3001
# will return {"status": "ok"}
# add a room name
$ curl -X POST -d '{"name": "room1"}' http://localhost:3001/create-room
# will return {"status":"room name successfully added"}
# view all rooms
$ curl http://localhost:3001/rooms
# will return {"rooms":["room1"]}
Using Postman
Check if the server is online
Adding a new room name
View all room names
Seems like all our endpoints are working fine.
However, the room names that we just stored are only persistent only when our server is running. If our server throws an error unexpectedly and exits, we will lose all our data. To make our storage persistent, we will introduce a database into our project.
In part 3, we will look at how to integrate the SQLite3 database into our application to make our data storage persistent. We will also add more handler functions to handle more request types.