The correct way to version Rails 3 APIs The correct way to version Rails 3 APIs json json

The correct way to version Rails 3 APIs


The project ActiveModel::Serializers has number issues related to Versioning, one of them provided an idea how to implement versioning via Namespace modules but it was closed 2 days ago followed by one of developer's words:

As you noticed we have discussed versioning on other issues and PR's as well, and I'm glad to read so really nice thought from all of you.

So the problem with AMS versioning does exist but not solved yet.

Back to the original question:

It's obvious that I can copy all of app/api/v1 to app/api/v2. I can then make my single change to my new v2 serializer. I've now got a large amount of duplicated code for a version 2 of an API that has made almost no changes. I need to maintain code in two places.

There is a compromise between inheritance complexity with side effects VS code duplication. In case of having well-tested V1 codebase that should be locked for any modification the maintenance does mean to have no errors while running regression test suite. The version 1 development cycle finished, tests written, contract behaviour signed off. The code duplication V1-V2 makes sense and it avoids regression failures.

I'll probably have to have my whole rspec suite run on the version 2 controllers as well as the version 1 ones with a tiny bit of extra testing for the v2 serializer. This sounds like a horrible idea.

I don't agree it's a horrible idea, this is a trade-off between expected behaviour and imaginary convenience with development. It's also not easy to avoid spec suite to be duplicated. Controllers, models can be reused but spec codebase will be more likely duplicated to be 100% confident that new changes don't break previous API version.

The best option that I can think of is to have a single controller (in this case probably just a single serializer) inside my v2 API, with some routing magic to check if a controller for the required version exists and to fall back through previous versions until it finds one.

Yes, this sounds good and helps to avoid application code (not spec suite though) duplication but requires an additional development efforts with maintenance. What you are trying to do called copy-on-write, only changes are copied over. This is well-known optimisation technique. Nevertheless the HTTP fallback sounds more appropriate.

Possibly I could analyse the namespaces for all controllers based on the filesystem and generate routes at rails boot time with fallbacks but it would be difficult to manage explicitly removing routes from a previous version of the API.

Imagine you have more than 2 versions of API, and a certain API call has 2 fallback ancestors where second is broken by developer's mistake, will you intercept not only 404 but 500 exceptions as well? What if latest DB scheme version breaks backward compatibility?

We have an additional unreleased API consisting of ~75 controllers covered by ~4000 specs. What happens when we start externally documenting and releasing these?

This is more like architecture question rather than specific implementation. If the API tends to be big, API design patterns can help to avoid building monolith API that can be difficult to support and maintain.

What would I recommend to do:

  • duplicate V1 to V2 entirely, rspec suite included
  • don't be afraid to spend 2x time on running tests
  • wait until AMS release a versioning (release v0.10.x)
  • split monolith API to the individual ones based

If code duplication is not acceptable the other option is to duplicate Rails app and deploy to the same server and dispatch requests with Nginx configuration:

location /v1 {  proxy_pass http://http://unix:/tmp/v1_backend.socket:/v1/;}location /v2 {  proxy_pass http://http://unix:/tmp/v2_backend.socket:/v2/;}

This particular code shows just for an example, I don't say it's a good idea to have 10 different Rails apps with each own version.

Back to original question, API versioning is difficult and for some API-clients it makes sense to have default (latest) API URL endpoint.


If I understood your all demands correctly, wouldn't this be sufficient solution for routing the v2 requests:

  1. Check for resource existence under v2.
  2. If not found, check that it isn't one of the disabled resources. If it is, return 404.
  3. Fallback for v1 resource if one is found.

Here's an example code (one scope for each step in the list above)

scope constraints: lambda { |request| request.url.split('api/')[1].split('/')[0] == 'v2' } do  # New resources introduced in v2end# Resource was not found in v2 API, check if it is removedscope constraints: lambda { |request| request.url.split('api/')[1].split('/')[0] == 'v2' } do  # Resources removed from v2  resources :resource1, to: proc { [404, {}, ['']] }end# Fallback for v2 routes that don't have v2 controller definedscope constraints: lambda { |request| ['v1', 'v2'].include?(request.url.split('api/')[1].split('/')[0]) } do  # Original v1 resourcesend

About the serializers, as you mentioned yourself those can be easily fixed by always providing new controller when changing them, or even by doing some magic that checks the version from the URL in default_serializer_options and sets serializer based on that.