gin-berry

Gin-berry is an effort to create an opinionated boilerplate for microservice development. It adds an extra layer on top of the gin framework as a config proxy to manage middlewares and payload validation in a more advanced way.

Why?

We wanted to rewrite an existing API/microservice with golang, but in go frameworks, we couldn’t find some key elements that we needed. After digging through, we realized that with golang, it’s easy to put a wrapper around an existing package and add additional functionality on top of it. This attempt is heavily inspired by fastify framework.

We wanted to have fastify’s Route options within the framework and implemented as an additional layer on top of gin. In addition, we wanted to modify the behavior of the middleware from the route handler when it’s defined. To do that, we created a Route method, and from that, we mapped the handlers to the actual gin routes.

To add some spice, we also integrated gorm as a database wrapper.

Features

  • Ability to add global or route group-specific middlewares.
  • Ability to add additional context value from route definition to a group middleware.
  • Ability to automatically validate query strings, params, and payloads.
  • Mimic fastify’s lifecycle hooks and ability to add preValidation, preHandler like functionality in a more clear way.
  • Ability to add custom error messages to the go-playground/validator.

Usage

We are wrapping the gin.Engine and gin.RouterGroup and creating a core.Service.

// setup service
service := core.New()

Or with a global middleware:

// setup service
service := core.New(func(context *gin.Context) {
    log.Println("Initial service middleware")
    context.Next()
})

The Service wraps gin.Default() so Logger and Recovery middlewares are automatically added.

You can also add as many middlewares as you wish to the core.Service

// setup service
service := core.New(func(context *gin.Context) {
    log.Println("Initial service middleware")
    context.Next()
}, CustomMiddleware(), AnotherMiddleware())

Now you can handle a request with the Route method.

service.Route("GET", "/", controllers.ServiceIndex())

controllers.ServiceIndex() is an instance of core.ServiceRouterConfig.

From the controller you’ll have the following for minimal handler:

import (
	"gin-berry/core"
	"gin-berry/models"
	"github.com/gin-gonic/gin"
)

func ServiceIndex() core.ServiceRouterConfig {
	return core.ServiceRouterConfig{
		Handler: func(ctx *gin.Context) {
			ctx.JSON(200, gin.H{
				"hello":  "world",
			})
		},
	}
}

A full example:

type RouteConfig struct {
  ForceAuth   bool
  ResolveUser bool
}

type QueryParams struct {
    Username string `validate:"required" json:"username" msg_required:"User name is required!"`
}

func ServiceIndex() core.ServiceRouterConfig {
  return core.ServiceRouterConfig{
    // these will be executed before the route handler
    // but after the group middleware
    Middlewares: []gin.HandlerFunc{func(ctx *gin.Context) {
        log.Println("Pre-route middleware")
    }},
    // this handles the actual route
    Handler: func(ctx *gin.Context) {
      var user models.User
      state, paging := user.GetUsers(1, 20)
      ctx.JSON(200, gin.H{
        "results": state,
        "paging":  paging,
      })
    },
    Options: core.ServiceRouterOptions{
        // we will require that a `Username` value must exist in the request query string.
        QueryString: QueryParams{},
    },
    Config: RouteConfig{
      ForceAuth:   true,
      ResolveUser: false,
    },
  }
}

core.ServiceRouterOptions can take the following to validate the incoming request.

type ServiceRouterOptions struct {
	QueryString interface{}
	Params      interface{}
	Body        interface{}
}

Acknowledgements

This is a work in progress. We are working on a lot of things, but we are not done yet.

Since I am still learning golang and the gin framework, this kind of a side project to learn more about the details of the language and the framework. That said, I believe it could be a good starting point for a microservice with a couple of more tweaks.

Still, it needs to be tested, and the additions that we made need to be measured in terms of performance impact. Especially the custom error message handling part is not well tested.

Contributors

metoikos kayakapagan

GitHub

View Github