Hola Amigos!!! Today we will be creating a straightforward REST API using Gin/Gonic and Service Weaver.
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
Service Weaver installed in your system
I would definitely recommend reading through some of the Service Weaver docs before reading further since some implementation and deployment aspects are better explained in the docs.
Introduction
Hey there, in this tutorial, we are going to be building a very rudimentary TODO REST API server, cause why not (again!!!)!! We would be using the gin/gonic framework to have an easy setup and use Service Weaver to make it incredibly easy to host the server in any environment or even across distributed systems without the need to understand all the difficult parts. I would be building a very similar project structure as seen in my other article, Simple API with Gin/Gonic and SurrealDB.
What is Service Weaver?
As stated on their website, in a gist, Service Weaver is a programming framework for writing, deploying, and managing distributed applications. You can run, test, and debug a Service Weaver application locally on your machine, and then deploy the application to the cloud with a single command.
Let's Build it ๐ ๏ธ
Create a folder for the project. I named my folder
gingonic-service-weaver
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.Then create the following folder structure. Ignore the files, some of them are auto-generated by weaver.
Let's start by creating the
main.go
file in the root directory. This file is the starting point for the service and it will look slightly different than a normal golang REST API project since we are using the service weaver framework to build as monolith and deploy as microservices. Below are the contents of the file.package main import ( "context" "log" "github.com/Atoo35/gingonic-service-weaver/api/routes" "github.com/ServiceWeaver/weaver" ) func main() { if err := weaver.Run(context.Background(), serve); err != nil { log.Fatal(err) } } type app struct { weaver.Implements[weaver.Main] server weaver.Listener } func serve(ctx context.Context, app *app) error { router := routes.SetupRoutes() return router.RunListener(app.server) }
The file import would change for the
api/routes
package depending on what you name it. Themain
function simply uses theserve
function to start the server and set the context for service weaver to understand.The struct
app
implements theMain
function of service weaver, which helps the framework understand the base structure of the project. It has an attributeserver
of typeweaver.Listener
which exposes the port and starts the server on that port.The
serve
function sets up the routes by calling theSetupRoutes
function that we would define in theroutes/routes.go
file later on. The catch here is instead of running gin/gonic viarouter.Run
function, we use theRunListener
function and pass in theweaver.Listener
type defined earlier in theapp
struct.Let's quickly create the
mock/tasks.go
file which would act like a database although it would be a static one.package mock import ( "github.com/Atoo35/gingonic-service-weaver/models" ) var Tasks = []models.Task{ { ID: "1", Title: "Task 1", Description: "Description 1", Completed: false, }, { ID: "2", Title: "Task 2", Description: "Description 2", Completed: false, }, { ID: "3", Title: "Task 3", Description: "Description 3", Completed: false, }, }
Also create the
models/task.go
file which represents the Task model which would ideally be a database table representation.package models type Task struct { ID string `json:"id,omitempty"` Title string `json:"title" binding:"required"` Description string `json:"description" binding:"required"` Completed bool `json:"completed"` }
Head over to
routes/routes.go
file and paste the below code. There's nothing fancy going on here, just the usual stuff.package routes import ( "github.com/Atoo35/gingonic-service-weaver/api/handlers" "github.com/gin-gonic/gin" ) func SetupRoutes() *gin.Engine { h := &handlers.TaskHandler{} 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 }
As you may have noticed, the
SetupRoutes
function is being called in the main.go file. This function simply creates a default gin router and creates all the API routes. We then attach the corresponding Handler function which should be called when a request is made.As the file file, create the
handlers/task.go
file and paste the below code.package handlers import ( "net/http" "strconv" "github.com/Atoo35/gingonic-service-weaver/mock" "github.com/Atoo35/gingonic-service-weaver/models" "github.com/gin-gonic/gin" ) type TaskHandler struct { } func (t *TaskHandler) GetTasks(gctx *gin.Context) { tasks := mock.Tasks gctx.JSON(http.StatusAccepted, gin.H{ "tasks": tasks, }) } func getTaskByID(id string) *models.Task { task := new(models.Task) for _, value := range mock.Tasks { if value.ID == id { task = &value break } } return task } func (t *TaskHandler) GetTask(gctx *gin.Context) { id := gctx.Param("id") task := getTaskByID(id) if task.ID != "" { gctx.JSON(http.StatusOK, gin.H{ "task": task, }) } else { gctx.JSON(http.StatusNotFound, gin.H{ "message": "Task not found", }) } } func (t *TaskHandler) CreateTask(gctx *gin.Context) { body := models.Task{} if err := gctx.ShouldBindJSON(&body); err != nil { gctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ "message": "Bad body", }) return } body.ID = strconv.Itoa(len(mock.Tasks) + 1) alltasks := append(mock.Tasks, body) gctx.JSON(http.StatusCreated, gin.H{ "tasks": alltasks, }) } func (t *TaskHandler) UpdateTask(gctx *gin.Context) { id := gctx.Param("id") task := getTaskByID(id) if task.ID == "" { gctx.AbortWithStatusJSON(http.StatusNotFound, gin.H{ "message": "Task not found", }) return } body := models.Task{} if err := gctx.ShouldBindJSON(&body); err != nil { gctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ "message": "Bad body", }) return } var result []models.Task for _, t := range mock.Tasks { if t.ID == id { result = append(result, body) } else { result = append(result, t) } } gctx.JSON(http.StatusCreated, gin.H{ "tasks": result, }) } func (t *TaskHandler) DeleteTask(gctx *gin.Context) { id := gctx.Param("id") task := getTaskByID(id) if task.ID == "" { gctx.AbortWithStatusJSON(http.StatusNotFound, gin.H{ "message": "Task not found", }) return } var result []models.Task for _, t := range mock.Tasks { if t.ID != id { result = append(result, t) } } gctx.JSON(http.StatusCreated, gin.H{ "tasks": result, }) }
Even this looks pretty straightforward and a standard handler code as found in any Golang REST API project.
That's literally it. We have written all the code for this monolith to de deployed as a single server or create replicas and have all the distributed systems problems taken care of by the Service Weaver library.
To run the project you first need to ask Weaver to generate the necessary code. This is done by the below command in the root directory.
weaver generate
Now you can run the project by using:
go run .
You will notice that the server is running on a very random port assigned by the system, for example here is mine:
Now this is upsetting and difficult to keep track of if it changes randomly on running everytime. Well fear not, the framework devs are smart and they knew we would be sad about this. Simply create a config.toml
file and paste the below code
[serviceweaver]
binary = "./gingonic-service-weaver"
[single]
listeners.server = {address="localhost:8080"}
Note: The
binary
file name should be replaced by the binary file generated by runninggo build
listeners.server
tells the weaver to run the server on port 8080
.
Now a better way of running the project is to first build the project by running
go build
and then running
weaver single deploy config.toml
And now you should have the service running on port 8080
.
That's it, you have successfully built a REST API server in golang using service weaver and gingonic!!! ๐
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.