<- Back to blog

Migrating a production Node.js app to Ruby on Rails

How to migrate a production app from Node.js to Ruby on Rails with zero downtime.

Vivek Patel

Vivek Patel

Moving a live backend is a tricky process. We recently migrated a large app from Node.js to Ruby on Rails. To avoid downtime, both backends had to run at the same time and share the exact same database.

To make this work, we forced our new Rails app to use a database schema designed for Node.

Here is how we bridged the gap, ported our logic, and finally cleaned up the code.

What we added for database compatibility

The legacy Node database did not follow standard Rails conventions. Table names were capitalized (like Organizations), primary keys had custom names, and columns used camelCase instead of snake_case.

Since the Node system was still running, we could not rename the tables right away. Instead, we added temporary configurations to our Rails models. We manually defined explicit table names, mapped column aliases, and specified explicit foreign keys for every association.

For example, a standard model during the transition looked like this:

class Organization < ApplicationRecord
  self.table_name = "Organizations"
  self.primary_key = "orgId"

  # Allows querying with Rails-y style, e.g., .where(id: [1, 2], company_name: "Acme")
  alias_attribute :id, :orgId
  alias_attribute :company_name, :companyName
  alias_attribute :billing_email, :billingEmail

  has_many :users, foreign_key: :orgId, inverse_of: :organization
end

Database queries also needed extra configuration. We had to explicitly reference legacy table names during joins to prevent SQL errors:

records.joins(:organization).where(Organization.table_name => { Organization.primary_key => ids, .. })

This added noise to the codebase, but it was necessary. It allowed us to route traffic to Rails incrementally while Node served the older endpoints.

Shared authentication with JWT

The Node app used JWT for authentication. To prevent users from getting logged out during the transition, we kept the same JWT secret. When a request hit the Rails API, Rails used this shared secret to decode the token and identify the user.

The page-by-page React migration

To keep the transition smooth, we migrated our React app page-by-page instead of doing a massive frontend rewrite. Our workflow looked like this:

  1. Identify required endpoints for a page.
  2. Implement those APIs in Rails based on the Node logic.
  3. Swap the API endpoint URL in the React frontend.
  4. Update the React components to match the new response schema from the Rails backend.

This kept our changes small and allowed us to ship updates continuously.

Introducing a test suite

The Node app had zero automated tests. As we ported APIs over, we built a test suite alongside them. By writing tests for every migrated endpoint, we ensured the new Rails backend was stable from day one.

What we removed after the migration

Once we turned off the Node backend, the database belonged entirely to Rails. We immediately ran a database migration to rename all tables, columns, and primary keys to match standard Rails conventions.

With the database normalized, we opened a final pull request to delete the temporary compatibility hacks. Now that the transition was complete, the bloated model shown above became drastically simpler:

class Organization < ApplicationRecord
  has_many :users
end

The cleanup cascaded through the app. We deleted hundreds of lines of aliases and simplified our database queries, replacing explicit table names with standard Rails syntax:

records.joins(:organization).where(organizations: { id: ids, .. })

That's it!

If you found this post useful, don't miss our latest tech posts. Keep innovating!

Have a project that needs help? Get in touch with us today!

Schedule a free consultation with our experts. Let's build, scale, and optimize your web application to achieve your business goals.