[Catalyst] REST and versioning

John Napiorkowski jjn1056 at yahoo.com
Wed Sep 18 16:31:14 GMT 2013




On Tue, Sep 17, 2013 at 10:12 AM, John Napiorkowski <jjn1056 at yahoo.com> wrote:
>
>
>
>
>>People seem to get religious over how to version their API.  I doubt I'd want to take sides on this but here's how I think we could do both side (URL version and content type versioning 
>
>
>Ya, I'm swayed by the Accept: header approach because in my mind /api/v1/account/123 and /api/v2/account/123 seems like different resources.   (Well, I guess they are.)  So, the versions are lazy way to make a new resource location.
>

I think the REST people would say yes, but I know a lot of APIs use URL versioning since its easy (or seems so off the top) and I also know there are some people that think it is clearly and absolutely the correct way to do it (I worked with one in a previous job).  Luckily Catalyst chaining and method matching I think makes this easy to do off the top.

I still think having it 'restfully correct' in your core code and then using some middleware for compatibility with dumb clients is probably safe.  I generally think its not a bad idea to have middleware for your APIs that map file extensions to accept types and map a query param for versioning.  For example

http://mycompany.com/api/user.json?v=1

would map internally a request like

http:://mycompany.com/api/user
http-accept application/vnd.mycompany.user.v1+json

or thereabouts.

>
>And even more so, seems like a dark path to go down.  Is just that individual resource versioned or is the entire API versioned?   And if it's the entire API, and the app needs to support multiple versions at the same time, then need a way for methods to "fall-back" to v1 when only a few methods change.
>
>
>Or maybe the client would have to know which methods are v2 vs. v1 and pick-and-choose.
>
>

yeah its a dark path I also agree.  This is why content negotiation and hyperlink driven application state is the most forward looking, but its not easy to do right and honestly what most people think of when they think of a web API is just an endpoint that takes and servers json data.  Application state is not important to that view (and is the common view for APIs that are driving a bit of interactivity on a webpage.


>
>
>Realistically, the problem that would likely come up is more related to client versioning where an old client cannot support some new feature of the API.   
>

The idea with rest is that this should be part of the content negotiation.  A client asks for a particular acceptable media type and the server decides how best to respond.  At least for the common server side negotiation.  

>
>For example,  say a service has a method to fetch a widget and a method to list them.
>
>
>GET /widget/123  # get widget 124
>GET /widget  # list all widgets.
>
>
>So, some client app is designed to list the widgets and then fetch them (for display or whatever).
>
>
>Later, the service is upgraded and adds a new type of widget -- and that is a type that the existing old client app cannot support.
>


Ideally you could properly support this backwardly if possible.  Generally you have two types of changes, one where you add in stuff, and that should be such that old clients shouldn't die.  but sometimes you need to really change things and that can get you into trouble.  I've noted this is worse when people are lazy and just think serializing the data structure to json and sending it over the wire is enough.  Like you have a version one user that gets serialized to json like

{ 
  'name': 'john',
  'phone': '2222222222',
}

but then you suddenly realize you need more than one phone per person and so you do:

{ 
  'name': 'john',
  'phone': [1111111,2222222,333333]
}

and of course that breaks the client.  either you are stuck doing

{ 
  'name': 'john',
  'phone': '2222222222',
  'additional_phones': [ ... ]
}

if you want to preserve backcompat or you change the version.  My thinking would be to do both really.

Of course this is a place where it would have been smart for the developer to have used an existing standard that already thought of all this stuff.  But what I've seen is APIs tend to be lazily modeled.  People oftern just serialized the output of a database query because the tools make that easy and it seems fast.  But down the road you get a lot of pain.

funny enough, this is a case where the 'bloated' xml might have been better

<contact>
  <name>john</name>
  <phone>1111111</phone>
  <phone>22222222</phone>
</contact>

If the client is smart and parsing the xml with something like css style selectors (or even xpath) then both versions should work.  If you are lazy and using something like XML-Simple then of course it will go bang just like with json.  But again I'd be likely to want to use something like chard instead.

>
>Does the service need to know what the client can support?
>

The client ideally would specify in the http accept header what they want and the server can decide if its willing to play, or pick up the marbles and go home :)

>
>sub widget_GET : Args(0) ... {
>    ...
>    push @include_types, $new_widget_type if $client_version >= 1.1;
>
>
>
>
>That's going to lead to some nasty spaghetti code over time.
>
>
>Maybe not such a great example as one could argue here that the client could GET /widget?type=1&type=2&type=3, but other changes might make that not so easy.
>
>

I should go look for some canonically 'correct' REST application and port it.

>
>
>In your chained example below how does that work with longer paths?
>
>
>I'm trying to come up with a good example.....
>
>
>/v1/document/1234   # fetch a document
>/v1/document/1234/share  # list who the document is shared with.
>
>
>Then what happens if it's just the share method that has a new version?
>


Well, if its a method, then that's RPC not REST and all constraints are off the table!

I think Catalyst chaining does long paths pretty well, although something chaining gets a bit confusing.  

>
>
>
>package Myapp::Web::Controller::API;
>>
>>use base 'Catalyst::Controller';
>>
>>sub start : ChainedParent
>> PathPrefix CaptureArgs(0)
>>{
>>  my ($self, $ctx) = @_;
>>}
>>
>>  sub version_one : Chained('start') PathPart('1') Args(0) { ... }
>>
>>  sub version_two : Chained('start') PathPart('2') Args(0) { ... }
>>
>>1;
>>
>>package Myapp::Web::Controller::API::1;
>>
>>use base 'Catalyst::Controller';
>>
>>sub start : ChainedParent
>> PathPrefix CaptureArgs(0)
>>{
>>  my ($self, $ctx) = @_;
>>}
>>
>>1;
>>
>>package Myapp::Web::Controller::API::2;
>>
>>use base 'Catalyst::Controller';
>>
>>sub start : ChainedParent
>> PathPrefix CaptureArgs(0)
>>{
>>  my ($self, $ctx) = @_;
>>}
>>
>>1;
>
>
> Bill Moseley
>moseley at hank.org  



More information about the Catalyst mailing list