Skip to content
This repository was archived by the owner on Jan 12, 2018. It is now read-only.

Commit dde2504

Browse files
committed
Complete README
1 parent 832cf3f commit dde2504

File tree

1 file changed

+169
-0
lines changed

1 file changed

+169
-0
lines changed

README.md

+169
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,172 @@
11
#tl;dr
22

33
This text describes the reasoning and deployment layout for the sample service implemented in this project.
4+
5+
# Background
6+
7+
## A Twitter chat with Peter Krantz
8+
9+
The 11th October 2011 Peter Krantz [@peterkz_swe](https://twitter.com/#!/peterkz_swe) posted a link to an article on [versioning REST API](http://thereisnorightway.blogspot.com/2011/02/versioning-and-types-in-resthttp-api.html):s to which I responded and we had a lengthy chat on the pros and cons of using custom media types to version services.
10+
11+
## My stance on versioning and ease of use
12+
13+
While I like the custom media type approach for being [REST](http://en.wikipedia.org/wiki/Representational_State_Transfer) "pure" I dislike it because it makes it harder to use the API for novice programmers (of which I have a great deal using my APIs at work). Also it makes exploring the API using a browser much more awkward and while I like and use [cURL](http://curl.haxx.se/) my browser window is always more readily at hand.
14+
15+
16+
17+
# Service versioning...
18+
19+
## ...using the URL
20+
21+
The upside of sticking a version number, and possibly even a format identifier, in the URL is obviously that it makes the service very easy to use from a browser. In addition it also makes the service very easy to use for novice programmers - something that might or might not be of utter importance depending on what kind of user base you're targeting. For example:
22+
23+
http://foo.bar/a-service/some-resource.v1.xml
24+
25+
## ...using custom media types
26+
27+
Specifying the version in the media type forces us to create a custom media type. We might, for example, chose to specify our media type as `application/vnd.baz-v1+xml` and our service invocation then has to specify this media type in the HTTP Accept header. For example:
28+
29+
GET /a-service/some-resource HTTP/1.1
30+
Host: foo.bar
31+
Accept: application/vnd.baz-v1+xml
32+
33+
## Implementation considerations (Java)
34+
35+
The custom media type maps quite naturally into Java based services using [Jersey](http://jersey.dev.java.net/) (and possibly other JVM based languages with Java interoperability) through Jersey's ability to use custom `MessageBodyWriter` classes. Using the URL versioning with optional format file extensions does not map as cleanly however.
36+
37+
## Operational considerations
38+
39+
I think there's a point in being able to have two versions of a service run in parallel on the same server (for my current environment this means in the same [Tomcat](http://tomcat.apache.org/) instance) but isolated from each other. I also think it might be good to separate disparate versions of a service into different "projects" instead of maintaining all versions of a service within the same code base.
40+
One way of solving this is by simply giving each service their own root context (for example by using [Tomcat's hash-sign thingy](http://tomcat.apache.org/tomcat-7.0-doc/config/context.html#Naming). But then we're back to putting versions in the URL which is considered a _bad thing_.
41+
42+
## Summing up the considerations
43+
44+
1. I want to implement versioning using custom media types since that provides a clean implementation.
45+
2. I want to allow exploratory interaction and support less tech savvy developers by allowing them to specify the version in the URL and the format as a file extension
46+
3. I want to separate different versions of the same service in the runtime and be able to deploy updates to a given service version without interrupting other services (and preferably without interrupting the service being deployed)
47+
48+
# Solution design
49+
50+
Achieving (1) is simple by doing a media type based implementation using Jersey.
51+
52+
I don't want to clutter my service with code to handle, optional, version and format specifiers in the URL. So to achieve (2) I need to make it appear as my service supports these things without actually changing my implementation - enter a HTTP proxy which in my case will be [nginx](http://nginx.org).
53+
54+
To achieve (3) this I will Tomcat and its #-naming support to give separate versions of each service their own root context. This will effectively put a version number in the URL of each instance. But this is a version number I don't want to force the outside world to know about - unless they choose to access the service by specifying the version in the URL. So again my HTTP proxy will have to cover my sorry ass.
55+
56+
## Tomcat deployment
57+
58+
Deploying the following two WAR files...
59+
60+
* rest-versioning#v1.war
61+
* rest-versioning#v2.war
62+
63+
...will give us the following root contexts...
64+
65+
* http://127.0.0.1:8080/rest-versioning/v1
66+
* http://127.0.0.1:8080/rest-versioning/v2
67+
68+
...which give us the following service endpoints...
69+
70+
* http://127.0.0.1:8080/rest-versioning/v1/person
71+
* http://127.0.0.1:8080/rest-versioning/v1/group
72+
* http://127.0.0.1:8080/rest-versioning/v2/person
73+
74+
## What's lacking
75+
76+
I want to publish the above service endpoints as versioned by media type...
77+
78+
* GET http://127.0.0.1:8080/rest-versioning/person
79+
* using `Accept: application/vnd.baz-v1+xml`
80+
* GET http://127.0.0.1:8080/rest-versioning/person
81+
* using `Accept: application/vnd.baz-v1+json`
82+
* GET http://127.0.0.1:8080/rest-versioning/person
83+
* using `Accept: application/vnd.baz-v2+xml`
84+
* GET http://127.0.0.1:8080/rest-versioning/person
85+
* using `Accept: application/vnd.baz-v2+json`
86+
* GET http://127.0.0.1:8080/rest-versioning/group
87+
* using `Accept: application/vnd.baz-v1+xml`
88+
* GET http://127.0.0.1:8080/rest-versioning/group
89+
* using `Accept: application/vnd.baz-v1+json`
90+
91+
...as well as URL-versioned...
92+
93+
* http://127.0.0.1:8080/rest-versioning/person.v1.xml
94+
* http://127.0.0.1:8080/rest-versioning/person.v1.json
95+
* http://127.0.0.1:8080/rest-versioning/person.v2.xml
96+
* http://127.0.0.1:8080/rest-versioning/person.v2.json
97+
* http://127.0.0.1:8080/rest-versioning/group.v1.xml
98+
* http://127.0.0.1:8080/rest-versioning/group.v1.json
99+
100+
...so that both requests to...
101+
102+
http://127.0.0.1:8080/rest-versioning/person
103+
Accept: application/vnd.baz-v1+xml
104+
105+
...and to...
106+
107+
http://127.0.0.1:8080/rest-versioning/person.v1.xml
108+
Accept: application/xml
109+
110+
...would end up being handled by...
111+
112+
http://127.0.0.1:8080/rest-versioning/v1/person
113+
114+
115+
## nginx deployment
116+
117+
Below is sample nginx configuration to make the above work for the sake of this text. However don't trust this configuration for your production environment.
118+
119+
server {
120+
listen 8183;
121+
server_name 127.0.0.1;
122+
proxy_redirect off;
123+
proxy_set_header host $http_host;
124+
proxy_set_header x-real-ip $remote_addr;
125+
126+
set $apiMime "$http_accept";
127+
# Figure out whether we're supposed to extract information from the URI
128+
if ($uri ~ ^.*\.(v\d)\.(xml|json)$) {
129+
set $apiVersion $1;
130+
set $apiMime "application/$2;charset=utf-8";
131+
132+
# Use the version information from the URL to proxy to the right
133+
# instance and strip the version and mime type information before
134+
# proxying to the actual service
135+
rewrite ^(.*)/(\w*).*$ $1/$apiVersion/$2 last;
136+
}
137+
# Figure out whether we're supposed to extract information from the Accept header
138+
if ($http_accept ~ ^application/vnd\.chids\.versioning-(v\d)\+(xml|json)) {
139+
set $apiVersion $1;
140+
# Use the version information from the accept header to proxy to
141+
# the right instance
142+
rewrite ^(.*)/(\w*)$ $1/$apiVersion/$2 last;
143+
}
144+
145+
proxy_set_header Accept $apiMime;
146+
location / {
147+
proxy_pass http://127.0.0.1:8080;
148+
}
149+
}
150+
151+
152+
## Testing
153+
154+
When running Tomcat on port 8080 (with the same WAR file deployed twice with different names - as specified above) and nginx with the above configuration you should be able to successfully access the service using:
155+
156+
curl -v http://127.0.0.1:8183/rest-versioning/person.v1.xml
157+
158+
...as well as:
159+
160+
curl -v --header 'Accept: application/vnd.chids.versioning-v1+json' http://127.0.0.1:8183/rest-versioning/person
161+
162+
163+
# Conclusion
164+
165+
At the time of this writing I'm leaning into this being pragmatic. And pragmatism is my overall goal, but I'm far from sure that this is the best way to achieve it. Comments and feedback are, as always, very welcome. Yell at me via Twitter [@martengustafson](http://www.twitter.com/martengustafson) or email me at [marten.gustafson@gmail.com](mailto:marten.gustafson@gmail.com).
166+
167+
# Other resources / see also
168+
169+
## Completely stand-alone services in Scala (using Jersey and Jetty)
170+
171+
[Coda Hale](http://codahale.com/) at Yammer have published what in my eyes appears to be a most excellent framework for building HTTP services called [Dropwizard](http://github.com/codahale/dropwizard) and I encourage you to have a look at it.
172+
The problem I currently face with the Dropwziard-style of services is that every service would have its own port number which when running multiple services on the same server might introduce some additional operational and deployment headaches most notably with the proxy configuration. It you're running in ze "cloud" (or otherwise virtualized environment) you could consider running one service per instance and having lots of small instances. Which would inevitable lead you to explore infrastructure automation using tools such as [Chef](http://www.opscode.com/) and [Puppet](http://puppetlabs.com/) if you haven't already. Which is a _good thing_. But I digress.

0 commit comments

Comments
 (0)