Build Status Dependency Status Coverage Status Maintainability

Pragma::Migration is an experiment at implementing Stripe-style API versioning.

This gem is highly experimental and still under active development. Usage in a production environment is strongly discouraged.


Add this line to your application’s Gemfile:

“by gem ‘pragma-migration’

And then execute:

$ bundle

Or install it yourself as:

$ gem install pragma-migration

Next, you’re going to create a migration repository for our API:

“by module API module V1 class MigrationRepository < Pragma::Migration::Repository # The initial version isn’t allowed to have migrations, because there is nothing # to migrate from. version ‘2017-12-17’ end end end

Finally, you need to mount the migration Rack middleware. In a Rails environment, this means adding the following to config/application.rb:

“by module YourApp class Application < Rails::Application # …

config.middleware.use Pragma::Migration::Middleware, 
  repository: API::V1::MigrationRepository,
  user_version_proc: (lambda do |request|
    # `request` here is a `Rack::Request` object.
    request.get_header 'X-Api-Version'

end end


When you start working on a new API version, you should define a new version in the repository:

“by module API module V1 class MigrationRepository < Pragma::Migration::Repository version ‘2017-12-17’

  # We will give this a date very far into the future for now, since we don't know the release
  # date yet. 
  version '2100-01-01', [
    # Add migrations here...

end end

Suppose you are working on a new API version and you decide to remove the _id suffix from association properties. In order to support users who are on an older version of the API, you will need to do the following:

  • remove the _id suffix from their requests;
  • add the _id suffix back to their responses.

To accomplish it, you might write a new migration like this:

“by module API module V1 module Migration class RemoveIdSuffixFromAuthorInArticles < Pragma::Migration::Base # You can use any pattern supported by Mustermann here. apply_to ‘/api/v1/articles/:id’

    # Optionally, you can write a description for the migration, which you can use for
    # documentation and changelogs.
    describe 'The _id suffix has been removed from the author property in the Articles API.'

    # The `up` method is called when a client on an old version makes a request, and should
    # convert the request into a format that can be consumed by the operation.
    def up
      request.update_param 'author', request.delete_param('author_id')

    # The `down` method is called when a response is sent to a client on an old version, and
    # should convert the response into a format that can be consumed by the client.
    def down
      parsed_body = JSON.parse(response.body.join(''))
        JSON.dump(parsed_body.merge('author' => parsed_body['author_id'])),

end end

Now, you will just add your migration to the repository:

“by module API module V1 class MigrationRepository < Pragma::Migration::Repository version ‘2017-12-17’

  version '2100-01-01', [

end end

As you can see, the migration allows API requests generated by outdated clients to run on the new version. You don’t have to implement ugly conditionals everywhere in your API: all the changes are neatly contained in the API migrations.

There is no limit to how many migrations or versions you can have. There’s also no limit on how old your clients can be: even if they are 10 versions behind, the migrations for all versions will be applied in order, so that the clients are able to interact with the very latest version without even knowing it!

Using migrations to contain side effects

In some cases, migrations are more complex than a simple update of the request and response.

Let’s take this example scenario: you are building a blog API and you are working on a new version that automatically sends an email to subscribers when a new article is sent, whereas the current version requires a separate API call to accomplish this. Since you don’t want to surprise existing users with the new behavior, you only want to do this when the new API version is being used.

You can use a no-op migration like the following for this:

“by module API module Migration module V1 class NotifySubscribersAutomatically < Pragma::Migration::Base describe ‘Subscribers are now notified automatically when a new article is published.’ end end end end

Then, in your operation, you will only execute the new code if the migration has been executed (i.e. the user’s version is greater than the migration’s version):

“by module API module V1 module Article module Operation class Create < Pragma::Operation::Create step :notify_subscribers!

      def notify_subscribers!(options)
        return unless migration_rolled?(Migration::NotifySubscribersAutomatically)

        # Notify subscribers here...

end end

Implementing complex version tracking

It is possible to implement more complex tracking strategies for determining your user’s API version. For instance, you might want to store the API version on the user profile instead:

“by module YourApp class Application < Rails::Application # …

config.middleware.use Pragma::Migration::Middleware, 
  repository: API::V1::MigrationRepository,
  user_version_proc: (lambda do |request|
    current_user = UserFinder.(request)
    current_user&.api_version # nil or an invalid value will default to the latest version

end end

The possibilities here are endless. Stripe adopts a hybrid strategy: they freeze a user’s API version when the user performs the first request. They allow the user to upgrade to newer versions either permanently (you are not allowed to go back after a grace period) or on a per-request basis, which is useful when doing partial upgrades.

This strategy can be accomplished quite easily with the following configuration:

“by module YourApp class Application < Rails::Application # …

config.middleware.use Pragma::Migration::Middleware, 
  repository: API::V1::MigrationRepository,
  user_version_proc: (lambda do |request|
    request.get_header('X-Api-Version') || UserFinder.(request)&.api_version

end end


Why are the migrations so low-level?

Admittedly, the code for migrations is very low-level: you are interacting with requests and responses directly, rather than using contracts and decorators. Unfortunately, so far we have been unable to come up with an abstraction that will not blow up at the first edge case. We are still experimenting here - ideas are welcome!

What are the drawbacks of API migrations?

If you are used to ActiveRecord migrations, then you might be tempted to use this very freely. However, API migrations are very different from DB migrations: DB migrations are run once and then forgotten forever, API migrations are executed on every request as long as clients are running on an outdated version of your API. This means that API migrations should be considered an active, evolving part of your codebase that you will have to maintain over time.

Why should I keep the /v1 prefix?

The main reason for keeping the /v1 prefix and the API::V1 namespace in your API is that you might want to introduce a change so disruptive that it warrants a separate major version, like migrating from REST to GraphQL or introducing one alongside the other. In this case, you won’t be able to use migrations to contain the change, so you will need to create a completely separate codebase and URL scheme.

What is the impact on performance?

We have a simple benchmark that runs 2,000 migrations in both directions. You can check out benchmark.rb for the details. Improvements are welcome!

Here are the results on my machine, a MacBook Pro 2017 i7 @ 3.1 GHz:

“ ruby -v benchmark.rb

ruby 2.4.2p198 (2017-09-14 revision 59899) [x86_64-darwin16]

Running 2k migrations, up and down:

   user     system      total        real

0.090000 0.010000 0.100000 ( 0.097414)

Are you out of your mind?

Possibly, but we’re not the only ones.


After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in gem_version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to


Bug reports and pull requests are welcome on GitHub at


The gem is available as open source under the terms of the MIT License.


  • [] Implement operation hooks
    • [] Pass Rack::Request object from Rails to operations (in pragma-rails)
    • [] Implement hooks (here or in pragma?)
  • [] Include and test in pragma-rails
  • [] Include in pragma-rails-starter
  • [] Allow to render an error and halt from user_version_proc
  • [] Abstraction to deal with decorators/contracts directly
    • [] Class-based pattern matching (in Base#apply_to)
    • [] Base#add_property, Base#transform_property etc.
  • [] Improve benchmark with different types of migrations
  • [] Figure out how to apply migrations on collections (e.g. Base.migrate_each?)