Versioning your API in Go
Tags: go, api
Published: 2018-12-24 08:33:26

---
title: Versioning your API in Go
published: true
description: 
tags: go, api
---

At some point in time your API need to have versions like `/v1` or `/v2` (like [github](https://developer.github.com/v3/) API).
To implement this in Go I will use [gorilla/mux](https://github.com/gorilla/mux) router and I will assume you have a functional [Go](https://golang.org/doc/install) 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](https://github.com/gorilla/mux#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](https://github.com/gorilla/mux#matching-routes) 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](https://github.com/geosoft1/api1) on github.

Enjoy.