API Versioning
All best practice conventions are implemented, including the suggestions written in the api-guidelines document.
- The version is retrieved from the
"Accept"
and/or"Accept-Version"
request header(s). - If a version matched, the server responds with a custom
"X-Api-Version"
header with the normalized version inside. - If a version was not found (missing version header or unimplemented version) the server responds with status code of 501 and plain text of "version not found" by default.
- If a specific version of the resource is deprecated by the server, the client receives the custom
"X-Api-Warn"
,"X-Api-Deprecation-Date"
and"X-Api-Deprecation-Info"
headers, the request is handled as expected.
Internally, the version validation is done by the regex-free and fast github.com/blang/semver/v4 third-party package.
Valid version ranges are:
- "<1.0.0"
- "<=1.0.0"
- ">1.0.0"
- ">=1.0.0"
- "1.0.0", "=1.0.0", "==1.0.0"
- "!1.0.0", "!=1.0.0"
A Range can consist of multiple ranges separated by space: Ranges can be linked by logical AND:
- ">1.0.0 <2.0.0" would match between both ranges, so "1.1.1" and "1.8.7" but not "1.0.0" or "2.0.0"
- ">1.0.0 <3.0.0 !2.0.3-beta.2" would match every version between 1.0.0 and 3.0.0 except 2.0.3-beta.2
Ranges can also be linked by logical OR:
- "<2.0.0 || >=3.0.0" would match "1.x.x" and "3.x.x" but not "2.x.x"
AND has a higher precedence than OR. It's not possible to use brackets.
Ranges can be combined by both AND and OR
>1.0.0 <2.0.0 || >3.0.0 !4.2.1
would match1.2.3
,1.9.9
,3.1.1
, but not4.2.1
,2.1.1
.
Import the
versioning
package.import (
// [...]
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/versioning"
)
Using the
versioning.NewGroup(version string) *versioning.Group
function you can create a group of routes to register the application's routes based on version requested by clients. The versioning.Group
completes the iris.Party
interface.Example Code:
package main
import (
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/versioning"
)
func main() {
app := iris.New()
app.OnErrorCode(iris.StatusNotFound, func(ctx iris.Context) {
ctx.WriteString(`Root not found handler.
This will be applied everywhere except the /api/* requests.`)
})
api := app.Party("/api")
// Optional, set version aliases (literal strings).
// We use `UseRouter` instead of `Use`
// to handle HTTP errors per version, but it's up to you.
api.UseRouter(versioning.Aliases(versioning.AliasMap{
// If no version provided by the client, default it to the "1.0.0".
versioning.Empty: "1.0.0",
// If a "latest" version is provided by the client,
// set the version to be compared to "3.0.0".
"latest": "3.0.0",
}))
// |----------------|
// | The fun begins |
// |----------------|
// Create a new Group, which is a compatible Party,
// based on version constraints.
v1 := versioning.NewGroup(api, ">=1.0.0 <2.0.0")
// Optionally, set custom view engine and path
// for templates based on the version.
v1.RegisterView(iris.HTML("./v1", ".html"))
// Optionally, set custom error handler(s) based on the version.
// Keep in mind that if you do this, you will
// have to register error handlers
// for the rest of the parties as well.
v1.OnErrorCode(iris.StatusNotFound, testError("v1"))
// Register resources based on the version.
v1.Get("/", testHandler("v1"))
v1.Get("/render", testView)
// Do the same for version 2 and version 3,
// for the sake of the example.
v2 := versioning.NewGroup(api, ">=2.0.0 <3.0.0")
v2.RegisterView(iris.HTML("./v2", ".html"))
v2.OnErrorCode(iris.StatusNotFound, testError("v2"))
v2.Get("/", testHandler("v2"))
v2.Get("/render", testView)
v3 := versioning.NewGroup(api, ">=3.0.0 <4.0.0")
v3.RegisterView(iris.HTML("./v3", ".html"))
v3.OnErrorCode(iris.StatusNotFound, testError("v3"))
v3.Get("/", testHandler("v3"))
v3.Get("/render", testView)
app.Listen(":8080")
}
func testHandler(v string) iris.Handler {
return func(ctx iris.Context) {
ctx.JSON(iris.Map{
"version": v,
"message": "Hello, world!",
})
}
}
func testError(v string) iris.Handler {
return func(ctx iris.Context) {
ctx.Writef("not found: %s", v)
}
}
func testView(ctx iris.Context) {
ctx.View("index.html")
}
Optional, set version aliases (literal strings).
api.UseRouter(versioning.Aliases(versioning.AliasMap{
// If no version provided by the client, default it to the "1.0.0".
versioning.Empty: "1.0.0",
// If a "latest" version is provided by the client,
// set the version to be compared to "3.0.0".
"latest": "3.0.0",
}))
We use
UseRouter
instead of Use
to handle HTTP errors per version, but it's up to you.The version is extracted through the
versioning.GetVersion
function. You can use it anywhere, even if you don't use the versioning feature. By default GetVersion
will try to read from:Accept
header, e.g.Accept: "application/json; version=1.0.0"
Accept-Version
header, e.g.Accept-Version: "1.0.0"
You can customize it by setting a version based on the request context:
api.Use(func(ctx *context.Context) {
if version := ctx.URLParam("version"); version != "" {
versioning.SetVersion(ctx, version)
}
ctx.Next()
})
Or by using the
FromQuery(version, default)
helper:api.Use(versioning.FromQuery("version", "1.0.0"))
To mark an API version as deprecated use the
Deprecated
method.v1.Deprecated(versioning.DefaultDeprecationOptions)
This will make the route handlers to send some extra headers to the client.
"X-API-Warn": options.WarnMessage
"X-API-Deprecation-Date": context.FormatTime(ctx, options.DeprecationDate))
"X-API-Deprecation-Info": options.DeprecationInfo
Last modified 2mo ago