A quick dive into fine-grained versioning

January 02, 2017 REST, Scala

Last week, I wrote a blog about fine-grained versioning. Today, I'll demonstrate this technique using a proof of concept written in Scala utilizing the powers of the magnificent Play Framework. I've chosen Scala as my hipster backend language as JavaScript is too busy upgrading to Angular 2 ;)

My POC contains a single REST endpoint, which returns a list of quotes. Due to a stupid hypothetical design mistake, I'm forced to support multiple versions. Remember my tip of last week!

To be concrete, we have to support two JSON structures; luckily, we only need to remap the output DTO. The backend remains the same. I'm not going to explain the whole codebase; you can see it on GitHub. Instead, I'll show you the interesting part.

object ApiAccepts {  
  val v1 = Accepting("application/vnd.QuotesV1+json")

  val v2 = Accepting("application/vnd.QuotesV2+json")
}

@Singleton
class QuotesResource @Inject()(quotesAction: QuotesAction,  
                               @Named("readQuotesActor") readQuotesActor: ActorRef)
                              (implicit ex: ExecutionContext)
  extends Controller {

  implicit val timeout: Timeout = 50.millis

  def all: Action[AnyContent] = quotesAction async {
    implicit request => (readQuotesActor ? All).
      mapTo[List[Quote]].
      map {
        quotes => render {

          case ApiAccepts.v1() => Ok {
            Json.toJson {
              quotes map { q => Json.obj(
                "ref" -> q.ref,
                "quote" -> q.quote,
                "author" -> q.author.name)
              }
            }
          } withHeaders ("Deprecated" -> s"Upgrade to ${ApiAccepts.v2.mimeType}") as ApiAccepts.v1.mimeType

          case ApiAccepts.v2() => Ok {
            Json.toJson {
              quotes map { q => Json.obj(
                "ref" -> q.ref,
                "quote" -> q.quote,
                "author" -> Json.obj(
                  "ref" -> q.author.ref,
                  "name" -> q.author.name,
                  "nationality" -> q.author.nationality))
              }
            }
          } as ApiAccepts.v2.mimeType

          case _ => UnsupportedMediaType
        }

      }
  }

}

As you can deduct, I've formulated two media types: "application/vnd.QuotesV1+json" and "application/vnd.QuotesV2+json".
The "vnd." part stands for vendor specific types; it's a convention.

Furthermore, you probably noticed a controller with a single method, which fetches data using AKKA's ask pattern and uses the rendering functionality of the Play Framework to respond to the client in the required format. The code is quite simple, so I don't believe further information is needed. Notice I also added a response header that indicates there is a newer version that the clients should begin using shortly when they ask for an old version.

So, let's give it a spin! I'll be using "HTTPie," a command line HTTP client.

 http http://localhost:9000/quotes Accept:application/vnd.QuotesV1+json
HTTP/1.1 200 OK  
Content-Length: 291  
Content-Type: application/vnd.QuotesV1+json  
Date: Sun, 01 Jan 2017 15:11:13 GMT  
Deprecated: Upgrade to application/vnd.QuotesV2+json  
Vary: Accept

[
    {
        "author": "Friedrich Nietzsche",
        "quote": "The future influences the present just as much as the past.",
        "ref": "6ebaa870-6ce2-4aa0-bbbf-985f929260a9"
    },
    {
        "author": "Albert Camus",
        "quote": "A man without ethics is a wild beast loosed upon this world.",
        "ref": "5684a75a-8a94-4141-8052-285656c556d8"
    }
]
http http://localhost:9000/quotes Accept:application/vnd.QuotesV2+json  
HTTP/1.1 200 OK  
Content-Length: 445  
Content-Type: application/vnd.QuotesV2+json  
Date: Sun, 01 Jan 2017 15:12:33 GMT  
Vary: Accept

[
    {
        "author": {
            "name": "Friedrich Nietzsche",
            "nationality": "German",
            "ref": "d91b9ac5-6b79-41f3-9ca6-d622a5dac937"
        },
        "quote": "The future influences the present just as much as the past.",
        "ref": "cb8e3929-8aa4-4107-aae8-8adad81c1362"
    },
    {
        "author": {
            "name": "Albert Camus",
            "nationality": "French",
            "ref": "5c4fd72d-0c0c-4fb0-9db9-5431303f5b7d"
        },
        "quote": "A man without ethics is a wild beast loosed upon this world.",
        "ref": "4ed70ca7-9e02-43fb-aa13-894aa4dbe32c"
    }
]

Well, that was easy! Using nothing more than content negotiation, we can successfully support multiple contracts for a single operation. Isn't that sweet?

To conclude, fine-grained versioning is far from rocket science and truly flexible when applied correctly. This technique is necessary when you develop your API using any Agile methodology. Furthermore, it's always a good idea to give consumers a decent fallback mechanism when breaking your API contracts. Moving the version to the operation rather than the API makes this a less painful balancing act.

Jeroen Bellen
Alken
Dissident blogger!