loading...

Versioning your API in Go

geosoft1 profile image George Calianu Updated on ・4 min read

At some point in time your API need to have versions like /v1 or /v2 (like github API).
To implement this in Go I will use gorilla/mux router and I will assume you have a functional Go environment.

We will make a new project with the following main.go file:

package main

import (
    "flag"
    "net/http"

    "github.com/gorilla/mux"
)

var (
    port = flag.String("port", "8080", "port")
)

func main() {
    flag.Parse()
    var router = mux.NewRouter()
    var api = router.PathPrefix("/api").Subrouter()
    api.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusNotFound)
    })
    http.ListenAndServe(":"+*port, router)
}

On short we have created a new router with a nice soubrouter for handling /api which represent the base of our versioned routes.
The routes will show like /api/v1/endpoint,/api/v2/endpoint and so on.

Also we have defined a not found handler attached to the subrouter who just returns a status code.

Note that we will use in the following various return codes to understand what routine is executed at each moment of time.

In this step we can attach a middleware to our subrouter to print what route is currently requested.

api.Use(func(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Println(r.RequestURI)
        next.ServeHTTP(w, r)
    })
})

Now we are ready to add our first version of API

var api1 = api.PathPrefix("/v1").Subrouter()
api1.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
})
api1.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusForbidden)
})

In the same manner like /api creation, add /v1 subrouter and coresponding not found handler. Note that the base of this subrouter is api subrouter not main router. Also we have defined the handler function for an endpoint named /status. Similarly we can create the /v2. Just paste this code and replace 1 with 2 and our code become

func main() {
    flag.Parse()
    var router = mux.NewRouter()
    var api = router.PathPrefix("/api").Subrouter()
    api.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusNotFound)
    })
    api.Use(func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            log.Println(r.RequestURI)
            next.ServeHTTP(w, r)
        })
    })
    var api1 = api.PathPrefix("/v1").Subrouter()
    api1.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
    })
    api1.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusForbidden)
    })
    var api2 = api.PathPrefix("/v2").Subrouter()
    api2.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusAccepted)
    })
    api2.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusNoContent)
    })
    http.ListenAndServe(":"+*port, router)
}

We are ready to test our API. For this run the project and use curl.

curl -I 'localhost:8080/api/'
HTTP/1.1 403 Not Found

curl -I 'localhost:8080/api/v1/'
HTTP/1.1 404 Forbidden

curl -I 'localhost:8080/api/v1/status'
HTTP/1.1 200 OK

curl -I 'localhost:8080/api/v2/'
HTTP/1.1 204 No Content

curl -I 'localhost:8080/api/v2/status'
HTTP/1.1 200 Accepted

Tests looks ok but we miss the authentications for our API. First idea is to simply use a MatcherFunc with a token and the following line

var api = router.PathPrefix("/api").Subrouter()

become

var api = router.MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) bool {
    return r.Header.Get("x-auth-token") == "admin"
}).PathPrefix("/api").Subrouter()

Test again

curl -I 'localhost:8080/api/v1/status' -H "x-auth-token: admin"
HTTP/1.1 202 OK

curl -I 'localhost:8080/api/v1/status' -H "x-auth-token: notadmin"
HTTP/1.1 404 Not Found

Not good, if i enter a wrong password i will receive not found error code. Basically is nothing wrong with this but i want to see that i'm not authorised. So, we will move the authentication code on the middleware and our finally code looks like that:

package main

import (
    "flag"
    "net/http"
    "log"

    "github.com/gorilla/mux"
)

var (
    port = flag.String("port", "8080", "port")
)

func main() {
    flag.Parse()
    var router = mux.NewRouter()
    var api = router.PathPrefix("/api").Subrouter()
    api.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusNotFound)
    })
    api.Use(func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if r.Header.Get("x-auth-token") != "admin" {
                w.WriteHeader(http.StatusUnauthorized)
                return
            }
            log.Println(r.RequestURI)
            next.ServeHTTP(w, r)
        })
    })
    var api1 = api.PathPrefix("/v1").Subrouter()
    api1.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
    })
    api1.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusForbidden)
    })
    var api2 = api.PathPrefix("/v2").Subrouter()
    api2.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusAccepted)
    })
    api2.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusNoContent)
    })
    http.ListenAndServe(":"+*port, router)
}

Final test

curl -I 'localhost:8080/api/v1/status' -H "x-auth-token: admin"
HTTP/1.1 200 OK

curl -I 'localhost:8080/api/v1/status' -H "x-auth-token: notadmin"
HTTP/1.1 401 Unauthorized

Yay, we did it. We have versioned API with some authentication. See the project on github.

Enjoy.

Discussion

pic
Editor guide