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()
}
}
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
).
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)
}
}
You can use below wrapper implementation for gin.
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")
}
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
}
/v1/user
curl --location 'localhost:8082/user' \
--header 'Accept-version: v1'
/v2/user
curl --location 'localhost:8082/user' \
--header 'Accept-version: v2'