Hey everyone, here I am back with another article. This article is a tutorial on how to develop a REST API backend in GO making use of gin/gonic framework and SurrealDB for the database
Here is the link to GitHub Repository. You can go ahead and get the code directly but I would recommend reading through the article once.
Prerequisites
GO installed in the system
Basic understanding of GO
Knowledge of REST APIs
SurrealDB installed in your system
Introduction
Hey there, in this tutorial we are going to be building a very rudimentary TODO REST API server, cause why not!! We would be using the gin/gonic framework to have an easy setup and use SurrealDB and its GO SDK to help ease the process of using it. Cheers to all the contributors of the SDK for making it smooth and doing the incredible heavy lifting for all of us (P.S.: I am one of them ๐). If you don't know what SurrealDB is, definitely recommend checking out their website, it's simply incredible and I loved using it as a database even for small side projects. Extremely light and easy to install and use.
Let's Build it ๐ ๏ธ
Create a folder for the project. I named my folder
gingonic-surrealdb
Use the command
go mod init github.com/<your username>/<either folder name or any name you want>
. It's not mandatory to have the entire GitHub link as the module name, you can keep it anything you want. It's just a best practice in the golang industry.Now create the following folder structure:
The
api
folder will contain code related to creating and handling the API calls.database
folder is self-explanatory and would contain code to connect to SurrealDB.models
represent the database models. Ignore thetmp
folder since that is generated automatically to log errors and build filesLet's start with the
database/database.go
file. Paste the below code into the file.package database import ( "log" "github.com/surrealdb/surrealdb.go" ) var DB *surrealdb.DB func Connect(connString, username, password string) { var err error DB, err = surrealdb.New(connString) if err != nil { log.Fatalf("Error connecting to database: %s", err) } _, err = DB.Signin(map[string]interface{}{ "user": username, "pass": password, }) if err != nil { log.Fatalf("Error signing in: %s", err) } if _, err = DB.Use("test", "tasks"); err != nil { log.Fatalf("Error using database: %s", err) } log.Printf("Connected to db with namespace %s and collection %s", "test", "tasks") }
The
import
statement is used to fetch the package required into the file and make use of it. Here we are using the built-inlog
package to add some logs into the terminal and thegithub.com/surrealdb/surrealdb.go
package which is the golang SDK for SurrealDB.Note: You might get an error when you copy paste the code. Simply run
'go get install "github.com/surrealdb/surrealdb.go"
in the terminal to install the package.The
Connect
function accepts the connection string and the username and password used to connect to the database. This username and password is set when you start the SurrealDB instance by using the below commandsurreal start -u root -p root
We set the
namespace
and the database name in this function.if _, err = DB.Use("test", "tasks"); err != nil { log.Fatalf("Error using database: %s", err) }
The namespace is
test
and the database name istasks
.Let's now create the
models/task.go
file. This file is simply a database model or a table in the database. The columns in the tables would be the attributes we set in this model.package models type Task struct { ID string `json:"id,omitempty"` Title string `json:"title"` Description string `json:"description"` Completed bool `json:"completed"` }
Now the most important implementation file. In the
handlers/task.go
file, paste the below code which handles all the interaction with the database. This is the file which would process all the incoming requests and serve the response accordingly.package handlers import ( "fmt" "net/http" "github.com/Atoo35/gingonic-surrealdb/database" "github.com/Atoo35/gingonic-surrealdb/models" "github.com/gin-gonic/gin" "github.com/surrealdb/surrealdb.go" ) type TaskHandler struct { DB *surrealdb.DB } func (h *TaskHandler) GetTasks(c *gin.Context) { tasks, err := database.DB.Select("tasks") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "message": fmt.Sprintf("Error while selecting tasks: %s", err.Error()), }) return } tasksSlice := new([]models.Task) err = surrealdb.Unmarshal(tasks, &tasksSlice) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "message": fmt.Sprintf("Error while unmarshalling tasks: %s", err.Error()), }) return } c.JSON(http.StatusAccepted, gin.H{ "tasks": tasksSlice, }) } func (h *TaskHandler) CreateTask(c *gin.Context) { task := new(models.Task) if err := c.ShouldBindJSON(task); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "message": fmt.Sprintf("Error while binding json: %s", err.Error()), }) return } _, err := database.DB.Create("tasks", task) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "message": fmt.Sprintf("Error while creating task: %s", err.Error()), }) return } c.JSON(http.StatusCreated, gin.H{ "task": task, }) } func getSingletask(id string) (interface{}, error) { task, err := database.DB.Select(id) if err != nil { return nil, err } fmt.Println(task) return task, nil } func (h *TaskHandler) GetTask(c *gin.Context) { id := c.Param("id") task, err := getSingletask(id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "message": fmt.Sprintf("Error while getting task: %s", err.Error()), }) return } taskModel := new(models.Task) err = surrealdb.Unmarshal(task, &taskModel) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "message": fmt.Sprintf("Error while unmarshalling task: %s", err.Error()), }) return } c.JSON(http.StatusAccepted, gin.H{ "task": taskModel, }) } func (h *TaskHandler) UpdateTask(c *gin.Context) { id := c.Param("id") task := new(models.Task) if err := c.ShouldBindJSON(task); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "message": fmt.Sprintf("Error while binding json: %s", err.Error()), }) return } _, err := getSingletask(id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "message": fmt.Sprintf("Error while getting task: %s", err.Error()), }) return } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "message": fmt.Sprintf("Error while unmarshalling task: %s", err.Error()), }) return } _, err = database.DB.Update(id, task) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "message": fmt.Sprintf("Error while updating task: %s", err.Error()), }) return } c.JSON(http.StatusAccepted, gin.H{ "task": task, }) } func (h *TaskHandler) DeleteTask(c *gin.Context) { id := c.Param("id") _, err := database.DB.Delete(id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "message": fmt.Sprintf("Error while deleting task: %s", err.Error()), }) return } c.JSON(http.StatusAccepted, gin.H{ "message": fmt.Sprintf("Task %s deleted", id), }) }
This entire file has 5 main functions,
GetTasks
,GetTask
(single task),CreateTask
,UpdateTask
,DeleteTask
. All the functions are relatively simple and just interact with the database correspondingly and serve the result or error if any. If you need some help in understanding the code please reach out to me or comment on the post and I would be happy to help you.Note: You would need to install gin/gonic here by running
go get install "github.com/gin-gonic/gin"
The next file is
routes/routes.go
. In this file we simply redirect the incoming request to the corresponding handler. Here is the code.package routes import ( "github.com/Atoo35/gingonic-surrealdb/api/handlers" "github.com/gin-gonic/gin" "github.com/surrealdb/surrealdb.go" ) func SetupRoutes(db *surrealdb.DB) *gin.Engine { h := &handlers.TaskHandler{DB: db} router := gin.Default() tasksRoutes := router.Group("/api/tasks") { tasksRoutes.GET("/", h.GetTasks) tasksRoutes.POST("/", h.CreateTask) tasksRoutes.GET("/:id", h.GetTask) tasksRoutes.PUT("/:id", h.UpdateTask) tasksRoutes.DELETE("/:id", h.DeleteTask) } return router }
We are grouping all the
tasks
related APIs by using therouter.Group()
function provided by gin/gonic for creating readable and simple routes.The final file is the
main.go
file in the root of the project. This file initializes the database and connects to it and starts the server.package main import ( "log" "github.com/Atoo35/gingonic-surrealdb/api/routes" "github.com/Atoo35/gingonic-surrealdb/database" ) func init() { database.Connect("ws://localhost:8000/rpc", "root", "root") } func main() { log.Printf("Woohooo") router := routes.SetupRoutes(database.DB) log.Println("Starting server on port 8080") log.Fatal(router.Run(":8080")) defer database.DB.Close() }
To run the application simply run
go run main.go
and you are set. The server should be running onhttp://localhost:8080
and you can now test the APIs by using postman or anything else if you prefer.
Do let me know in the comments if something is not working as expected or if there is some issue.
Support
If you liked my article, consider supporting me with a coffee โ๏ธ or some crypto ( โฟ, โ , etc)
Here is my public address 0x7935468Da117590bA75d8EfD180cC5594aeC1582
Let's Connect
Feedback
Let me know if I have missed something or provided the wrong info. It helps me keep genuine content and learn.