API Versioning

The versioning subpackage provides semver versioning for your APIs.

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.

Introduction

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 match 1.2.3, 1.9.9, 3.1.1, but not 4.2.1, 2.1.1.

Getting Started

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")
}

Version Aliases

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.

Get Current Version

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"))

Deprecation

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 updated