From Rails 5 to 7: A Complete Guide to a Modern Upgrade
Are you still running Ruby on Rails 5 and wondering if it's time to upgrade?
With Rails 7 bringing major changes like Hotwire, Turbo, and new JavaScript handling, the upgrade might seem overwhelming. But don't worry - this Rails 7 upgrade guide will walk you through everything step by step.
Upgrading from Rails 5 to Rails 7 isn't just about staying current. It's about modernization, better performance, improved DX (developer experience), and avoiding the pitfalls of legacy code.
Let's dive into this comprehensive guide that covers everything from JavaScript migration to handling STI gotchas.
What's Changed Between Rails 5 and Rails 7?
Before jumping into the upgrade process, let's understand what major Rails 7 features and changes you'll encounter:
The Big Changes
Zeitwerk Autoloader: Rails 6 introduced Zeitwerk, a thread-safe autoloader that replaces the classic Rails autoloader. Rails 7 removes the old autoloader completely, so you must adapt your code.
JavaScript Revolution: Rails 7 completely changes how you handle JavaScript. Webpacker is gone, replaced by Importmap (default) or lightweight bundlers like esbuild. This affects every Rails app's frontend changes.
Hotwire by Default: Rails 7 embraces Hotwire (HTML-over-the-wire) with Turbo and Stimulus as the default JavaScript framework. This replaces Turbolinks and Rails UJS.
Multiple Database Support: Enhanced support for multiple database connections and new frameworks like Action Mailbox and Action Text.
Performance Improvements: Better query optimization, async loading capabilities, and overall performance boosts.
Step 1: Handling Autoloading Changes and STI Gotchas
One of the first challenges in any Rails 7 upgrade guide is dealing with Zeitwerk. If your app doesn't follow Rails conventions, you'll hit issues immediately.
Key Zeitwerk Requirements
Consistent File Naming: Classes must match their file names and namespaces exactly. A class Admin::User should be in app/models/admin/user.rb with proper module wrapping.
Remove Manual Requires: Delete any require_dependency calls in your code. Zeitwerk handles autoloading automatically.
Eager Loading in Production: Zeitwerk eager loads everything in production. If you have initialization code that references constants at boot, wrap it in a Rails.application.config.to_prepare block.
Single Table Inheritance (STI) Considerations
STI models present a special challenge with Zeitwerk. Here's how to handle them:
The Problem: If STI subclasses aren't loaded when you fetch records, you might get UnknownType errors or records falling back to the base class.
Solution 1 - Eager Load in Development:
# config/environments/development.rb
Rails.application.configure do
# Reload application's code on every request. This slows down response time
# but is perfect for development since you don't have to restart the web server
# when you make code changes.
config.cache_classes = false
# Do not eager load code on boot.
config.eager_load = true
# Show full error reports.
config.consider_all_requests_local = true
# Other common development config...
end
Solution 2 - Selective STI Loading:
# config/initializers/preload_sti.rb
Rails.application.config.to_prepare do
Rails.autoloaders.main.eager_load_dir("#{Rails.root}/app/models/vehicle")
end
Pro Tip: Run bin/rails zeitwerk:check after upgrading to catch autoloading issues before booting the app.
Step 2: JavaScript Migration - From Webpacker to Modern Solutions
This is where most Rails 5 to Rails 7 upgrades get tricky. The entire JavaScript handling system has changed.
Understanding Your Options
Importmap (Default): Manages JavaScript modules without a bundler. Perfect for simpler apps or those mainly using Rails-provided JS.
JS Bundling: Use jsbundling-rails with esbuild, Rollup, or Webpack for apps with heavy custom JavaScript or frameworks like React.
Removing Webpacker
First, clean up the old system:
- Remove webpacker gem from your Gemfile
- Delete config/webpacker.yml and config/webpack/* files
- Remove app/javascript/packs directory
- Delete Webpacker binstubs (bin/webpack, bin/webpack-dev-server)
- Remove package.json and yarn.lock if not needed
Adding New JavaScript System
For Importmap:
# Gemfile
gem "importmap-rails"
gem "turbo-rails"
gem "stimulus-rails"
gem "sprockets-rails" # Don't forget this!
For esbuild:
# Gemfile
gem "jsbundling-rails"
gem "turbo-rails"
gem "stimulus-rails"
Updating Your Layout
Replace old JavaScript includes:
<%= javascript_pack_tag "application" %>
<%= javascript_importmap_tags %>
<%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
Managing JavaScript Dependencies
With Importmap:
# ✅ For Importmap in Rails 7+ — pinning a JS package without using yarn or npm
./bin/importmap pin lodash
With Bundler:
# ✅ Traditional JavaScript package management using npm
npm install lodash
# Or with yarn
yarn add lodash
Step 3: Embracing Hotwire - Turbo and Stimulus
Rails 7 makes Hotwire the default way to build interactive applications. Here's what you need to know:
Understanding Hotwire Components
Turbo Drive: Replaces Turbolinks for progressive navigation Turbo Frames: For independent page sections that update separately
Turbo Streams: Real-time partial page updates Stimulus: Lightweight JavaScript framework for HTML enhancement
Migrating from Turbolinks and Rails UJS
If your Rails 5 app used Turbolinks and Rails UJS, Turbo replaces both:
// ✅ Old Turbolinks event (Rails 5/6)
document.addEventListener("turbolinks:load", function() {
// Your code here
console.log("Turbolinks loaded!");
});
// ✅ New Turbo event (Rails 7+ with turbo-rails)
document.addEventListener("turbo:load", function() {
// Your code here
console.log("Turbo loaded!");
});
Forms with remote: true now automatically work with Turbo and expect Turbo Stream responses.
Opting Out of Hotwire
If you're not ready for Hotwire:
```html
<!-- 🔧 Disable Turbo on a specific element -4->
<div data-turbo="false">
<!-- This block will bypass Turbo Drive (full page reload) -->
<!-- Use this when you need default browser behavior for links or forms inside -->
</div>
```
Or remove turbo-rails and stimulus-rails gems entirely and add back Rails UJS manually.
Step 4: Integrating React or Vue in Rails 7
Many Rails 5 apps include React or Vue components. You can still use these in Rails 7:
Choose the Right Build Strategy
For React/Vue apps, use a bundler instead of Importmap:
{
"scripts": {
// 📦 Bundles all JS from app/javascript and outputs to app/assets/builds
"build": "esbuild app/javascript --bundle --outdir=app/assets/builds --loader:.jsx=jsx"
}
}
Modern Alternatives
Consider using Vite with vite_ruby gem or shakapacker (community-maintained Webpacker for Rails 7) for better React integration.
Step 5: Breaking Changes and API Updates
Rails 7 removes several deprecated features. Here are the most common issues:
Method Deprecations
user.update_attributes(name: "Alice")
user.update(name: "Alice")
Controller Changes
render plain: "hello" # replaces :text
head :ok # replaces :nothing => true (renders only headers)
render plain: "hello" # Sends plain text response
head :ok # Sends headers with 200 OK, no body
ActiveRecord Stricter Behavior
Rails 7 adds stricter validations and query handling:
- belongs_to associations required by default
- Ambiguous column references raise errors
- Optional strict loading to catch N+1 queries
Configuration Updates
Run rails app:update after each version bump to get new configuration defaults. This updates:
- Environment configuration files
- New initializers
- Asset pipeline settings
- Database configuration format
Step 6: Testing Your Upgraded Application
Your test suite is crucial during the upgrade process:
Test Framework Compatibility
RSpec: Update to RSpec 5+ for Rails 7 support Minitest: Should work with minimal changes
System Test Updates
Rails 7 with Turbo might change page loading behavior:
expect(page).to have_content("Success", wait: 5)
Parallel Testing
Rails 6+ supports parallel testing out of the box. Enable it for faster test runs:
# test/test_helper.rb
parallelize(workers: :number_of_processors)
Step 7: Performance and Developer Experience Improvements
Rails 7 brings significant performance improvements:
Async Query Loading
# Rails 5 - Sequential queries
data1 = Model.fetch_heavy_data
data2 = OtherModel.fetch_heavy_data
# Rails 7 - Parallel queries
relation1 = Model.fetch_heavy_data.load_async
relation2 = OtherModel.fetch_heavy_data.load_async
# ...do other work...
results1 = relation1.to_a
results2 = relation2.to_a
Enhanced Developer Experience
- Better error pages and console
- Improved IRB with autocompletion
- bin/dev command for running multiple processes
- Thread-safe code reloading with Zeitwerk
Step 8: Deployment Considerations for Docker and Heroku
Ruby Version Updates
Update your deployment configuration:
# Gemfile
ruby "3.0.3" # or newer
Heroku Buildpacks
With Importmap (no Node needed):
- Only heroku/ruby buildpack required
With JS Bundling:
heroku buildpacks:add heroku/nodejs
heroku buildpacks:add heroku/ruby
Environment Configuration
Set required environment variables:
heroku config:set RAILS_MASTER_KEY=your_master_key
Step 9: Your Complete Upgrade Checklist
Follow this step-by-step process for a smooth upgrade:
Preparation Phase
- Test Coverage: Ensure 80%+ test coverage of critical functionality
- Ruby Upgrade: Move to Ruby 3.x first, test on Rails 5
- Backup: Create database backups and code branches
Incremental Upgrade Process
- Rails 5 → 6.0:
- Update Gemfile: gem "rails", "~> 6.0.0"
- Run bundle update rails
- Execute rails app:update and merge changes
- Fix autoloading issues (temporarily use config.autoloader = :classic if needed)
- Address deprecation warnings
- Rails 6.0 → 6.1:
- Update Gemfile: gem "rails", "~> 6.1.0"
- Run rails app:update again
- Enable Zeitwerk: config.autoloader = :zeitwerk
- Run bin/rails zeitwerk:check
- Fix remaining deprecations
- Rails 6.1 → 7.0:
- Update Gemfile: gem "rails", "~> 7.0.0"
- Remove Webpacker, add new JavaScript gems
- Run appropriate installer (rails importmap:install or rails javascript:install:esbuild)
- Update layout JavaScript includes
- Remove Turbolinks/UJS, verify Turbo works
- Run rails app:update for final config changes
Post-Upgrade Tasks
- Dependency Updates: Update all gems to Rails 7 compatible versions
- Staging Deployment: Test in production-like environment
- Production Deployment: Deploy with database migrations
- Monitoring: Watch logs and performance metrics closely
Common Pitfalls to Avoid
- Don't skip intermediate versions - upgrade 5→6.0→6.1→7.0
- Don't ignore deprecation warnings - fix them before the next version
- Don't upgrade Ruby and Rails simultaneously - do Ruby first
- Don't forget to test JavaScript behavior - Turbo changes page interactions
Troubleshooting Common Issues
Autoloading Errors
- Run bin/rails zeitwerk:check to identify naming issues
- Ensure file names match class/module names exactly
- Check for missing module wrappers in namespaced files
JavaScript Not Working
- Verify correct JavaScript include tags in layout
- Check browser console for import errors
- Ensure asset pipeline or bundler is generating files correctly
STI Model Issues
- Implement STI preloading strategies shown above
- Test all STI subclass instantiation after upgrade
Performance Regressions
- Enable strict loading to catch N+1 queries: User.strict_loading.includes(:posts)
- Use Rails 7's async query loading for parallel database operations
- Monitor memory usage during upgrade testing
Conclusion
Upgrading from Rails 5 to Rails 7 is a significant modernization effort, but the benefits are substantial. You'll gain better performance, improved DX, modern JavaScript handling with Hotwire, and enhanced security features. The key is taking it step by step, testing thoroughly, and embracing the new Rails way of building applications.
Ready to modernize your Rails application? At TechDots, we specialize in Rails upgrades and can help you navigate this complex process smoothly. Contact us today to discuss your Rails 7 upgrade project and ensure a seamless transition to modern Rails development.