Gin/Gonic + Service Weaver

Gin/Gonic + Service Weaver

REST API using GO

ยท

7 min read

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 ๐Ÿ› ๏ธ

  1. Create a folder for the project. I named my folder gingonic-service-weaver

  2. 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.

  3. Then create the following folder structure. Ignore the files, some of them are auto-generated by weaver.

  4. 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. The main function simply uses the serve function to start the server and set the context for service weaver to understand.

    The struct app implements the Main function of service weaver, which helps the framework understand the base structure of the project. It has an attribute server of type weaver.Listener which exposes the port and starts the server on that port.

    The serve function sets up the routes by calling the SetupRoutes function that we would define in the routes/routes.go file later on. The catch here is instead of running gin/gonic via router.Run function, we use the RunListener function and pass in the weaver.Listener type defined earlier in the app struct.

  5. 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,
         },
     }
    
  6. 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"`
     }
    
  7. 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.

  8. 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 running go 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

Buy Me A Coffee

Let's Connect

Github

LinkedIn

Twitter

Feedback

Let me know if I have missed something or provided the wrong info. It helps me keep genuine content and learn.

Did you find this article valuable?

Support Atharva Deshpande by becoming a sponsor. Any amount is appreciated!

ย