{"id":13878186,"url":"https://github.com/keygen-sh/request_migrations","last_synced_at":"2025-11-11T18:39:41.277Z","repository":{"id":38948220,"uuid":"506710871","full_name":"keygen-sh/request_migrations","owner":"keygen-sh","description":"Write request and response migrations for Stripe-like versioning of your Ruby on Rails API. Make breaking changes without breaking things!","archived":false,"fork":false,"pushed_at":"2025-11-04T16:10:27.000Z","size":156,"stargazers_count":137,"open_issues_count":3,"forks_count":2,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-11-04T18:10:31.772Z","etag":null,"topics":["api-migration","api-versioning","rails","rails-api","rails-gem","ruby","ruby-on-rails"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/keygen-sh.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"ezekg"}},"created_at":"2022-06-23T16:19:55.000Z","updated_at":"2025-11-04T16:10:30.000Z","dependencies_parsed_at":"2022-08-09T06:37:12.838Z","dependency_job_id":"12b18c79-9d84-41f8-9582-8c27e12ead02","html_url":"https://github.com/keygen-sh/request_migrations","commit_stats":{"total_commits":52,"total_committers":1,"mean_commits":52.0,"dds":0.0,"last_synced_commit":"93eb0d0f22bce8dc4b6777a74b0982b73481fa85"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/keygen-sh/request_migrations","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/keygen-sh%2Frequest_migrations","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/keygen-sh%2Frequest_migrations/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/keygen-sh%2Frequest_migrations/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/keygen-sh%2Frequest_migrations/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/keygen-sh","download_url":"https://codeload.github.com/keygen-sh/request_migrations/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/keygen-sh%2Frequest_migrations/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":283910127,"owners_count":26915128,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-11-11T02:00:06.610Z","response_time":65,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api-migration","api-versioning","rails","rails-api","rails-gem","ruby","ruby-on-rails"],"created_at":"2024-08-06T08:01:42.191Z","updated_at":"2025-11-11T18:39:41.251Z","avatar_url":"https://github.com/keygen-sh.png","language":"Ruby","funding_links":["https://github.com/sponsors/ezekg"],"categories":["Ruby"],"sub_categories":[],"readme":"# request_migrations\n\n[![CI](https://github.com/keygen-sh/request_migrations/actions/workflows/test.yml/badge.svg)](https://github.com/keygen-sh/request_migrations/actions)\n[![Gem Version](https://badge.fury.io/rb/request_migrations.svg)](https://badge.fury.io/rb/request_migrations)\n\n**Make breaking API changes without breaking things!** Use `request_migrations` to craft\nbackwards-compatible migrations for API requests, responses, and more. Read [the blog\npost](https://keygen.sh/blog/breaking-things-without-breaking-things/).\n\nThis gem was extracted from [Keygen](https://keygen.sh) and is being used in production\nto serve millions of API requests per day.\n\n![request_migrations diagram](https://user-images.githubusercontent.com/6979737/175964358-a2d8951d-46c6-4962-9f5e-0569cbf5972e.png)\n\nSponsored by:\n\n\u003ca href=\"https://keygen.sh?ref=request_migrations\"\u003e\n  \u003cdiv\u003e\n    \u003cimg src=\"https://keygen.sh/images/logo-pill.png\" width=\"200\" alt=\"Keygen\"\u003e\n  \u003c/div\u003e\n\u003c/a\u003e\n\n_A fair source software licensing and distribution API._\n\nLinks:\n\n- [Installing request_migrations](#installation)\n- [Supported Ruby versions](#supported-rubies)\n- [RubyDoc](#documentation)\n- [Usage](#usage)\n  - [Response migrations](#response-migrations)\n  - [Request migrations](#request-migrations)\n  - [Data migrations](#data-migrations)\n  - [Routing constraints](#routing-constraints)\n  - [Configuration](#configuration)\n  - [Version formats](#version-formats)\n- [Testing](#testing)\n- [Tips and tricks](#tips-and-tricks)\n- [Examples](#examples)\n- [Credits](#credits)\n- [Contributing](#contributing)\n- [License](#license)\n\n## Installation\n\nAdd this line to your application's `Gemfile`:\n\n```ruby\ngem 'request_migrations'\n```\n\nAnd then execute:\n\n```bash\n$ bundle\n```\n\nOr install it yourself as:\n\n```bash\n$ gem install request_migrations\n```\n\n## Supported Rubies\n\n**`request_migrations` supports Ruby 3.1 and above.** We encourage you to upgrade if you're on an older\nversion. Ruby 3 provides a lot of great features, like better pattern matching and a new shorthand\nhash syntax.\n\n## Documentation\n\nYou can find the documentation on [RubyDoc](https://rubydoc.info/github/keygen-sh/request_migrations).\n\n_We're working on improving the docs._\n\n## Features\n\n- Define migrations for migrating a response between versions.\n- Define migrations for migrating a request between versions.\n- Define migrations for applying data migrations.\n- Define version-based routing constraints.\n- It's fast.\n\n## Usage\n\nUse `request_migrations` to make _backwards-incompatible_ changes in your code, while\nproviding a _backwards-compatible_ interface for clients on older API versions. What\nexactly does that mean? Well, let's demonstrate!\n\nLet's assume that we provide an API service, which has `/users` CRUD resources.\n\nLet's also assume we start with the following `User` model:\n\n```ruby\nclass User\n  include ActiveModel::Model\n  include ActiveModel::Attributes\n\n  attribute :name, :string\nend\n```\n\nAfter awhile, we realize our `User` model's combined `name` attribute is not working too\nwell, and we want to change it to `first_name` and `last_name`.\n\nSo we write a database migration that changes our `User` model:\n\n```ruby\nclass User\n  include ActiveModel::Model\n  include ActiveModel::Attributes\n\n  attribute :first_name, :string\n  attribute :last_name, :string\nend\n```\n\nBut what about the API consumers who were relying on `name`? We just broke our API contract\nwith them! To resolve this, let's create our first request migration.\n\nWe recommend that migrations be stored under `app/migrations/`.\n\n```ruby\nclass CombineNamesForUserMigration \u003c RequestMigrations::Migration\n  # Provide a useful description of the change\n  description %(transforms a user's first and last name to a combined name attribute)\n\n  # Migrate inputs that contain a user. The migration should mutate\n  # the input, whatever that may be.\n  migrate if: -\u003e data { data in type: 'user' } do |data|\n    first_name = data.delete(:first_name)\n    last_name  = data.delete(:last_name)\n\n    data[:name] = \"#{first_name} #{last_name}\"\n  end\n\n  # Migrate the response. This is where you provide the migration input.\n  response if: -\u003e res { res.successful? \u0026\u0026 res.request.params in controller: 'api/v1/users',\n                                                                 action: 'show' } do |res|\n    data = JSON.parse(res.body, symbolize_names: true)\n\n    # Call our migrate definition above\n    migrate!(data)\n\n    res.body = JSON.generate(data)\n  end\nend\n```\n\nAs you can see, with pattern matching, it makes creating migrations for certain\nresources simple. Here, we've defined a migration that only runs for the `users#show`\nresource, and only when the response is successful. In addition, the data is\nonly migrated when the response body contains a user.\n\nNext, we'll need to configure `request_migrations` via an initializer under\n`initializers/request_migrations.rb`:\n\n```ruby\nRequestMigrations.configure do |config|\n  # Define a resolver to determine the target version. Here, you can perform\n  # a lookup on the current user using request parameters, or simply use\n  # a header like we are here, defaulting to the latest version.\n  config.request_version_resolver = -\u003e request {\n    request.headers.fetch('Foo-Version') { config.current_version }\n  }\n\n  # Define the latest version of our application.\n  config.current_version = '1.1'\n\n  # Define previous versions and their migrations, in descending order.\n  config.versions = {\n    '1.0' =\u003e %i[combine_names_for_user_migration],\n  }\nend\n```\n\nLastly, you'll want to update your application controller so that migrations\nare applied:\n\n```ruby\nclass ApplicationController \u003c ActionController::API\n  include RequestMigrations::Controller::Migrations\n\n  # Optionally rescue from requests for unsupported versions\n  rescue_from RequestMigrations::UnsupportedVersionError, with: -\u003e {\n    render(\n      json: { error: 'unsupported API version requested', code: 'INVALID_API_VERSION' },\n      status: :bad_request,\n    )\n  }\nend\n```\n\nNow, when an API client provides a `Foo-Version: 1.0` header, they'll receive a\nresponse containing the combined `name` attribute.\n\n### Response migrations\n\nWe covered this above, but response migrations define a change to a response.\nYou define a response migration by using the `response` class method.\n\n```ruby\nclass RemoveVowelsMigration \u003c RequestMigrations::Migration\n  description %(in the past, we had a bug that removed all vowels, and some clients rely on that behavior)\n\n  response if: -\u003e res { res.request.params in action: 'index' | 'show' | 'create' | 'update' } do |res|\n    body = JSON.parse(res.body, symbolize_names: true)\n\n    # Mutate the response body by removing all vowels\n    body.deep_transform_values! { _1.gsub(/[aeiou]/, '') }\n\n    res.body = JSON.generate(body)\n  end\nend\n```\n\nThe `response` method accepts an `:if` keyword, which should be a lambda\nthat evaluates to a boolean, which determines whether or not the migration\nshould be applied. An `ActionDispatch::Response` will be yielded, the\ncurrent response (calls `controller#response`).\n\nThe gem makes no assumption on a response's content type or what the migration\nwill do. You could, for example, migrate the response body, or mutate the\nheaders, or even change the response's status code.\n\nThe `response` method can be used multiple times per-migration.\n\n### Request migrations\n\nRequest migrations define a change on a request. For example, modifying a request's\nheaders. You define a response migration by using the `request` class method.\n\n```ruby\nclass AssumeContentTypeMigration \u003c RequestMigrations::Migration\n  description %(in the past, we assumed all requests were JSON, but that has since changed)\n\n  # Migrate the request, adding an assumed content type to all requests.\n  request do |req|\n    req.headers['Content-Type'] = 'application/json'\n  end\nend\n```\n\nThe `request` method accepts an `:if` keyword, which should be a lambda\nthat evaluates to a boolean, which determines whether or not the migration\nshould be applied. An `ActionDispatch::Request` object will be yielded,\nthe current request (calls `controller#request`).\n\nAgain, like with response migrations, the gem makes no assumption on what\na migration does. A migration could mutate a request's params, or mutate\nheaders. It's up to you, all it does is provide the request.\n\nRequest migrations should [avoid using the `migrate` method](#avoid-migrate-for-request-migrations).\n\nThe `request` method can be used multiple times.\n\n### Data migrations\n\nIn our first scenario, where we combined our user's name attributes, we defined\nour migration using the `migrate` class method. At this point, you may be wondering\nwhy we did that, since we didn't use that method for the 2 previous request and\nresponse migrations above.\n\nWell, it comes down to support for data migrations (as well as offering a nice\ninterface for pattern matching inputs). Let's go back to our first example,\n`CombineNamesForUserMigration`.\n\n```ruby\nclass CombineNamesForUserMigration \u003c RequestMigrations::Migration\n  # Provide a useful description of the change\n  description %(transforms a user's first and last name to a combined name attribute)\n\n  # Migrate inputs that contain a user. The migration should mutate\n  # the input, whatever that may be.\n  migrate if: -\u003e data { data in type: 'user' } do |data|\n    first_name = data.delete(:first_name)\n    last_name  = data.delete(:last_name)\n\n    data[:name] = \"#{first_name} #{last_name}\"\n  end\n\n  # Migrate the response. This is where you provide the migration input.\n  response if: -\u003e res { res.successful? \u0026\u0026 res.request.params in controller: 'api/v1/users' | 'api/v1/me',\n                                                                 action: 'show' } do |res|\n    data = JSON.parse(res.body, symbolize_names: true)\n\n    # Call our migrate definition above\n    migrate!(data)\n\n    res.body = JSON.generate(data)\n  end\nend\n```\n\nWhat if we had [a webhook system](https://keygen.sh/blog/how-to-build-a-webhook-system-in-rails-using-sidekiq/)\nthat we also needed to apply these migrations to? Well, we can use a data migration\nhere, via the `Migrator` class:\n\n```ruby\nclass WebhookWorker\n  def perform(event, endpoint, data)\n    # ...\n\n    # Migrate event data from latest version to endpoint's configured version\n    current_version = RequestMigrations.config.current_version\n    target_version  = endpoint.api_version\n    migrator        = RequestMigrations::Migrator.new(\n      from: current_version,\n      to: target_version,\n    )\n\n    # Migrate the event data (tries to apply all matching migrations)\n    migrator.migrate!(data:)\n\n    # ...\n\n    event.send!(data)\n  end\nend\n```\n\nThis will apply the block defined in `migrate` onto our data. With that,\nwe've successfully applied a migration to both our API responses, as well\nas to the webhook events we send. In this case, if our event data matches\nour expected data shape, e.g. `type: 'user'`, then the migration will\nbe applied.\n\nIn addition to data migrations, this allows for easier [testing](#testing).\n\nThe `migrate` method can be used multiple times per-migration to e.g. \nmatch and migrate on different shapes of data. For a JSON:API app,\nfor example, you could migrate on `data: [*]` and `includes: [*]`.\n\n### Routing constraints\n\nWhen you want to encourage API clients to upgrade, you can utilize a routing `version_constraint`\nto define routes only available for certain versions.\n\nYou can also utilize routing constraints to remove an API endpoint entirely.\n\n```ruby\nRails.application.routes.draw do\n  # This endpoint is only available for version 1.1 and above\n  version_constraint '\u003e= 1.1' do\n    resources :some_shiny_new_resource\n  end\n\n  # Remove this endpoint for any version below 1.1\n  version_constraint '\u003c 1.1' do\n    scope module: :v1x0 do\n      resources :a_deprecated_resource\n    end\n  end\nend\n```\n\nCurrently, routing constraints only work for the `:semver` version format. (PRs welcome!)\n\n### Configuration\n\n```ruby\nRequestMigrations.configure do |config|\n  # Define a resolver to determine the target version. Here, you can perform\n  # a lookup on the current user using request parameters, or simply use\n  # a header like we are here, defaulting to the latest version.\n  config.request_version_resolver = -\u003e request {\n    request.headers.fetch('Foo-Version') { config.current_version }\n  }\n\n  # Define the accepted version format. Default is :semver.\n  config.version_format = :semver\n\n  # Define the latest version of our application.\n  config.current_version = '1.2'\n\n  # Define previous versions and their migrations, in descending order.\n  # Should be a hash, where the key is the version and the value is an\n  # array of migration symbols or classes.\n  config.versions = {\n    '1.1' =\u003e %i[\n      has_one_author_to_has_many_for_posts_migration\n      has_one_author_to_has_many_for_post_migration\n    ],\n    '1.0' =\u003e %i[\n      combine_names_for_users_migration\n      combine_names_for_user_migration\n    ],\n  }\n\n  # Use a custom logger. Supports ActiveSupport::TaggedLogging.\n  config.logger = Rails.logger\nend\n```\n\n### Version formats\n\nBy default, `request_migrations` uses a `:semver` version format, but it can be configured\nto instead use one of the following, set via `config.version_format=`.\n\n| Format     |                                                      |\n|:-----------|:-----------------------------------------------------|\n| `:semver`  | Use semantic versions, e.g. `1.0`, `1.1`, and `2.0`. |\n| `:date`    | Use date versions, e.g. `2020-09-02`, `2021-01-01`.  |\n| `:integer` | Use integer versions, e.g. `1`, `2`, and `3`.        |\n| `:float`   | Use float versions, e.g. `1.0`, `1.1`, and `2.0`.    |\n| `:string`  | Use string versions, e.g. `a`, `b`, and `z`.         |\n\nAll versions will be sorted according to the format's type.\n\n## Testing\n\nUsing data migrations allows for easier testing of migrations. For example, using Rspec:\n\n```ruby\ndescribe CombineNamesForUserMigration do\n  before do\n    RequestMigrations.configure do |config|\n      config.current_version = '1.1'\n      config.versions        = {\n        '1.0' =\u003e [CombineNamesForUserMigration],\n      }\n    end\n  end\n\n  it 'should migrate user name attributes' do\n    migrator = RequestMigrations::Migrator.new(from: '1.1', to: '1.0')\n    data     = serialize(\n      create(:user, first_name: 'John', last_name: 'Doe'),\n    )\n\n    expect(data).to include(type: 'user', first_name: 'John', last_name: 'Doe')\n    expect(data).to_not include(name: anything)\n\n    migrator.migrate!(data:)\n\n    expect(data).to include(type: 'user', name: 'John Doe')\n    expect(data).to_not include(first_name: 'John', last_name: 'Doe')\n  end\nend\n```\n\nTo avoid polluting the global configuration, you can use `RequestMigrations::Testing`\nwithin your application's `spec/rails_helper.rb`, or a similar spec helper:\n\n```ruby\nrequire 'request_migrations/testing'\n\nRspec.configure do |config|\n  config.before :each do\n    RequestMigrations::Testing.setup!\n  end\n\n  config.after :each do\n    RequestMigrations::Testing.teardown!\n  end\nend\n```\n\nThis will setup a new test configuration, and then restore the previous global configuration\nafter each spec.\n\n## Tips and tricks\n\nOver the years, we're learned a thing or two about versioning an API. We'll share tips here.\n\n### Use pattern matching\n\nPattern matching really cleans up the `:if` conditions, and overall makes migrations more readable.\n\n```ruby\nclass AddUsernameAttributeToUsersMigration \u003c RequestMigrations::Migration\n  description %(adds username attributes to a collection of users)\n\n  migrate if: -\u003e body { body in data: [*] } do |body|\n    case body\n    in data: [*, { type: 'users', attributes: { ** } }, *]\n      body[:data].each do |user|\n        case user\n        in type: 'users', attributes: { email: }\n          user[:attributes][:username] = email\n        else\n        end\n      end\n    else\n    end\n  end\n\n  response if: -\u003e res { res.successful? \u0026\u0026 res.request.params in controller: 'api/v1/users',\n                                                                 action: 'index' } do |res|\n    body = JSON.parse(res.body, symbolize_names: true)\n\n    migrate!(body)\n\n    res.body = JSON.generate(body)\n  end\nend\n```\n\nJust be sure to remember your `else` block when `case` pattern matching. :)\n\n### Route helpers\n\nIf you need to use route helpers in a migration, include them in your migration:\n\n```ruby\nclass SomeMigration \u003c RequestMigrations::Migration\n  include Rails.application.routes.url_helpers\nend\n```\n\n### Separate by shape\n\nDefine separate migrations for different input shapes, e.g. define a migration for an `#index`\nto migrate an array of objects, and define another migration that handles the singular object\nfrom `#show`, `#create` and `#update`. This will help keep your migrations readable.\n\nFor example, for a singular user response:\n\n```ruby\nclass CombineNamesForUserMigration \u003c RequestMigrations::Migration\n  description %(transforms a user's first and last name to a combined name attribute)\n\n  migrate if: -\u003e data { data in type: 'user' } do |data|\n    first_name = data.delete(:first_name)\n    last_name  = data.delete(:last_name)\n\n    data[:name] = \"#{first_name} #{last_name}\"\n  end\n\n  response if: -\u003e res { res.successful? \u0026\u0026 res.request.params in controller: 'api/v1/users',\n                                                                 action: 'show' } do |res|\n    data = JSON.parse(res.body, symbolize_names: true)\n\n    migrate!(data)\n\n    res.body = JSON.generate(data)\n  end\nend\n```\n\nAnd for a response containing a collection of users:\n\n```ruby\nclass CombineNamesForUsersMigration \u003c RequestMigrations::Migration\n  description %(transforms a collection of users' first and last names to a combined name attribute)\n\n  migrate if: -\u003e data { data in [*, { type: 'user' }, *] do |data|\n    data.each do |record|\n      case record\n      in type: 'user', first_name:, last_name:\n        record[:name] = \"#{first_name} #{last_name}\"\n\n        record.delete(:first_name)\n        record.delete(:last_name)\n      else\n      end\n    end\n  end\n\n  response if: -\u003e res { res.successful? \u0026\u0026 res.request.params in controller: 'api/v1/users',\n                                                                 action: 'index' } do |res|\n    data = JSON.parse(res.body, symbolize_names: true)\n\n    migrate!(data)\n\n    res.body = JSON.generate(data)\n  end\nend\n```\n\nNote that the `migrate` method now migrates an array input, and matches on the `#index` route.\n\n### Always check response status\n\nAlways check a response's status. You don't want to unintentionally apply migrations to error\nresponses.\n\n```ruby\nclass SomeMigration \u003c RequestMigrations::Migration\n  response if: -\u003e res { res.successful? } do |res|\n    # ...\n  end\nend\n```\n\nAlso mind `204 No Content`, since the response body will be `nil`.\n\n### Don't match on URL pattern\n\nDon't match on URL pattern. Instead, use `response.request.params` to access the request params\nin a `response` migration, and use the `:controller` and `:action` params to determine route.\n\n```ruby\nclass SomeMigration \u003c RequestMigrations::Migration\n  # Bad\n  response if: -\u003e res { res.request.path.matches?(/^\\/v1\\/posts$/) }\n\n  # Good\n  response if: -\u003e res { res.request.params in controller: 'api/v1/posts', action: 'index' }\nend\n```\n\n### Namespace deprecated controllers\n\nWhen you need to entirely change a controller or service class, use a `V1x0::UsersController`-style\nnamespace to keep the old deprecated classes tidy.\n\n```ruby\nclass V1x0::UsersController\n  def foo\n    # Some old foo action\n  end\nend\n```\n\n### Avoid migrate for request migrations\n\nAvoid using `migrate` for request migrations. If you do, then data migrations, e.g. for\nwebhooks, will attempt to apply the request migrations. This may erroneously produce bad\noutput, or even undo a response migration. Instead, keep all request migration logic,\ne.g. transforming params, inside of the `request` block.\n\n```ruby\nclass SomeMigration \u003c RequestMigrations::Migration\n  # Bad (side-effects for data migrations)\n  migrate do |params|\n    params[:foo] = params.delete(:bar)\n  end\n\n  request do |req|\n    migrate!(req.params)\n  end\n\n  # Good\n  request do |req|\n    req.params[:foo] = req.params.delete(:bar)\n  end\nend\n```\n\n### Avoid routing contraints\n\nAvoid using routing version constraints that remove functionality. They can be a headache\nduring upgrades. Consider only making _additive_ changes. Instead, consider removing or\nhiding the documentation for old or deprecated endpoints, to limit any new usage.\n\n```ruby\nRails.application.routes.draw do\n  resources :users do\n    # Iffy\n    version_constraint '\u003c 1.1' do\n      resources :posts\n    end\n\n    # Good\n    scope module: :v1x0 do\n      resources :posts\n    end\n  end\nend\n```\n\n### Avoid n+1s\n\nAvoid introducing n+1 queries in your migrations. Try to utilize the current data you have\nto perform more meaningful queries, returning only the data needed for the migration.\n\n```ruby\nclass AddRecentPostToUsersMigration \u003c RequestMigrations::Migration\n  description %(adds :recent_post association to a collection of users)\n\n  # Bad (n+1)\n  migrate if: -\u003e data { data in [*, { type: 'user' }, *] do |data|\n    data.each do |record|\n      case record\n      in type: 'user', id:\n        recent_post = Post.reorder(created_at: :desc)\n                          .find_by(user_id: id)\n\n        record[:recent_post] = recent_post\u0026.id\n      else\n      end\n    end\n  end\n\n  # Good\n  migrate if: -\u003e data { data in [*, { type: 'user' }, *] do |data|\n    user_ids = data.collect { _1[:id] }\n    post_ids = Post.select(:id, :user_id)\n                   .distinct_on(:user_id)\n                   .where(user_id: user_ids)\n                   .reorder(created_at: :desc)\n                   .group_by(\u0026:user_id)\n\n    data.each do |record|\n      case record\n      in type: 'user', id: user_id\n        record[:recent_post] = post_ids[user_id]\u0026.id\n      else\n      end\n    end\n  end\n\n  response if: -\u003e res { res.successful? \u0026\u0026 res.request.params in controller: 'api/v1/users',\n                                                                 action: 'index' } do |res|\n    data = JSON.parse(res.body, symbolize_names: true)\n\n    migrate!(data)\n\n    res.body = JSON.generate(data)\n  end\nend\n```\n\nInstead of potentially tens or hundreds of queries, we make a single purposeful query\nto get the data we need in order to complete the migration.\n\n---\n\nHave a tip of your own? Open a pull request!\n\n## Examples\n\nBelow are some real-world examples of request migrations:\n\n- Migrations: https://github.com/keygen-sh/keygen-api/tree/master/app/migrations\n- Tests: https://github.com/keygen-sh/keygen-api/tree/master/spec/migrations\n\n## Is it any good?\n\nYes.\n\n## Credits\n\nCredit goes to Stripe for inspiring the [high-level migration strategy](https://stripe.com/blog/api-versioning).\nIntercom has [another good post on the topic](https://www.intercom.com/blog/api-versioning/).\n\n## Contributing\n\nIf you have an idea, or have discovered a bug, please open an issue or create a pull request.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkeygen-sh%2Frequest_migrations","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkeygen-sh%2Frequest_migrations","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkeygen-sh%2Frequest_migrations/lists"}