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.
Top comments (0)