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.