Search code examples
gogo-gin

Header-based versioning on golang


I would like to implement header-based versioning on go using gin. I'm thinking to do this on the router using a middleware function.

The client will call the same API URL, and the version will be on a custom HTTP header, like this:

To call version 1 GET /users/12345678 Accept-version: v1

To call version 2: GET /users/12345678 Accept-version: v2

So, the router can identify the header and call the specific version. Something like this:

            router := gin.Default()

            v1 := router.Group("/v1")
            v1.Use(VersionMiddleware())
            v1.GET("/user/:id", func(c *gin.Context) {
                c.String(http.StatusOK, "This is the v1 API")
            })

            v2 := router.Group("/v2")
            v2.Use(VersionMiddleware())
            v2.GET("/user/:id", func(c *gin.Context) {
                c.String(http.StatusOK, "This is the v2 API")
            })

func VersionMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        version := c.Request.Header.Get(configuration.GetConfigValue("VersionHeader"))

        // Construct the new URL path based on the version number
        path := fmt.Sprintf("/%s%s", version, c.Request.URL.Path)

        // Modify the request URL path to point to the new version-specific endpoint
        c.Request.URL.Path = path
        c.Next()
    }
}


Solution

  • Please check the below code snippet. I used ReverseProxy to redirect to the given version. You need to validate given version carefully. Otherwise, it will cause a recursive call.

    Note: I used two versions of /user GET (/v1/user and /v2/user).

    Sample Code

    package main
    
    import (
     "net/http"
     "net/http/httputil"
     "regexp"
    
     "github.com/gin-gonic/gin"
    )
    
    
    
    func main() {
     router := gin.Default()
     router.Use(VersionMiddleware())
    
    
     v1 := router.Group("/v1")
     v1.GET("/user", func(c *gin.Context) {
      c.String(http.StatusOK, "This is the v1 API")
     })
    
     v2 := router.Group("/v2")
     v2.GET("/user", func(c *gin.Context) {
      c.String(http.StatusOK, "This is the v2 API")
     })
    
     router.Run(":8082")
    }
    
    
    
    func VersionMiddleware() gin.HandlerFunc {
        return func(c *gin.Context) {
      
      // You need to check c.Request.URL.Path whether 
      // already have a version or not, If it has a valid
      // version, return.
      regEx, _ := regexp.Compile("/v[0-9]+")
      ver := regEx.MatchString(c.Request.URL.Path)
      if ver {
       return
      }
    
      version := c.Request.Header.Get("Accept-version")
      
      // You need to validate  given version by the user here.
      // If version is not a valid version, return error 
      // mentioning that given version is invalid.
    
      director := func(req *http.Request) {
        r := c.Request
        req.URL.Scheme = "http"
        req.URL.Host = r.Host
        req.URL.Path =  "/"+ version + r.URL.Path
        }
      proxy := &httputil.ReverseProxy{Director: director}
      proxy.ServeHTTP(c.Writer, c.Request)
     }
    }
    

    OR

    You can use below wrapper implementation for gin.

    • Example
    
    package main
    
    import (
     "net/http"
    
     "github.com/gin-gonic/gin"
     "github.com/udayangaac/stackoverflow/golang/75860989/ginwrapper"
    )
    
    
    
    func main() {
      engine := gin.Default()
     router := ginwrapper.NewRouter(engine)
    
     defaultRouter := router.Default()
     defaultRouter.Get("/profile",func(ctx *gin.Context) {
    
     })
    
     v1 := router.WithVersion("/v1")
     v1.Get("/user",func(ctx *gin.Context) {
      ctx.String(http.StatusOK, "This is the profile v1 API")
     })
    
     v2 := router.WithVersion("/v2")
     v2.Get("/user",func(ctx *gin.Context) {
      ctx.String(http.StatusOK, "This is the profile v2 API")
     })
    
     
     engine.Run(":8082")
    }
    
    • Wrapper
    package ginwrapper
    
    import (
     "fmt"
     "net/http"
    
     "github.com/gin-gonic/gin"
    )
    
    type Router struct {
     router *gin.Engine
     versionGroups map[string]*gin.RouterGroup
    }
    
    type VersionedRouter struct {
     version string
     Router
    }
    
    func NewRouter(router *gin.Engine) *Router {
     return &Router{
      router: router,
      versionGroups: make(map[string]*gin.RouterGroup),
     }
    }
    
    func (a *Router) Default() VersionedRouter {
     return VersionedRouter{Router: *a }
    }
    
    func  (a *Router) WithVersion(version string) VersionedRouter {
     if _,ok := a.versionGroups[version]; ok {
      panic("cannot initialize same version multiple times")
     }
     a.versionGroups[version] = a.router.Group(version)
     return VersionedRouter{Router: *a,version:version }
    }
    
    
    
    
    func (vr VersionedRouter) Get(relativePath string, handlers ...gin.HandlerFunc)  {
     vr.handle(http.MethodGet,relativePath,handlers...)
    }
    
    // Note: You need to follow the same for other HTTP Methods.
    // As an example, we can write a method for Post HTTP Method as below,
    // 
    //  func (vr VersionedRouter) Post(relativePath string, handlers ...gin.HandlerFunc)  {
    //   vr.handle(http.MethodPost,relativePath,handlers...)
    //  }
    
    
    
    
    
    func (vr VersionedRouter)handle(method,relativePath string, handlers ...gin.HandlerFunc)  {
     if !vr.isRouteExist(method,relativePath) {
      vr.router.Handle(method,relativePath,func(ctx *gin.Context) {
       version := ctx.Request.Header.Get("Accept-version")
       if len(version) == 0 {
        ctx.String(http.StatusBadRequest,"Accept-version header is empty")
       }
       ctx.Request.URL.Path = fmt.Sprintf("/%s%s", version, ctx.Request.URL.Path)
       vr.router.HandleContext(ctx)
      })
     }
    
     versionedRelativePath := vr.version + relativePath
     if !vr.isRouteExist(method,versionedRelativePath) {
      vr.router.Handle(method,versionedRelativePath,handlers... )
     }
    }
    
    
    func (a VersionedRouter) isRouteExist(method,relativePath string) bool {
     for _,route := range a.router.Routes() {
      if route.Method == method && relativePath == route.Path {
       return true
      } 
     }
     return false
    }
    
    

    Sample Requests

    • /v1/user
    curl --location 'localhost:8082/user' \
    --header 'Accept-version: v1'
    
    • /v2/user
    curl --location 'localhost:8082/user' \
    --header 'Accept-version: v2'