Understanding the Rails to Sinatra Migration
Migrating from Ruby on Rails to Sinatra means replacing the full-stack Rails framework with a lightweight, minimalistic web DSL. Sinatra is not a framework in the traditional sense — it’s a library that gives you just enough to route HTTP requests and return responses. This migration is typically chosen for applications that have outgrown Rails' complexity, or for services that need to become leaner, faster to boot, and easier to maintain in a microservice architecture.
Why it matters: Rails provides a wealth of conventions, helpers, and built‑in layers (ActiveRecord, ActionPack, asset pipeline, etc.). For large, monolithic apps, that’s powerful. But as the application is split into smaller services, or when you realise most of that machinery is unnecessary, Rails becomes overhead. Sinatra offers a path to strip away unused abstractions, reduce memory footprint, and improve startup times. The migration is also a chance to rethink boundaries, clean up legacy code, and adopt a more modular design.
Preparing for the Migration
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Before writing any Sinatra code, audit your existing Rails application thoroughly. Identify exactly which parts of Rails you actually rely on, and decide how each will be handled in Sinatra:
- Routing & Controllers — Rails routes, controllers, and filters.
- Models & Database — ActiveRecord (or another ORM), migrations, validations.
- Views & Helpers — ERB/Haml templates, layouts, partials, helper methods.
- Authentication & Sessions — Devise, cookie‑based sessions, CSRF protection.
- Assets — Sprockets pipeline, JavaScript/CSS bundling.
- Mailers, Background Jobs, WebSockets — Any extra Rails components.
- Testing — RSpec, fixtures, system tests.
Make a checklist. Not everything must be ported directly; some features can be re‑implemented as external services (e.g., dedicated auth service), or simplified drastically. The goal is to end up with a Sinatra application that does exactly what’s needed — nothing more.
Step 1: Setting Up a Sinatra Skeleton
Start with a minimal project structure. Create a new directory and add a Gemfile, a config.ru for Rack, and a main application file (often app.rb). Here’s a basic setup:
# Gemfile
source 'https://rubygems.org'
gem 'sinatra', '~> 3.0'
gem 'sinatra-contrib', '~> 3.0' # for extensions like reloader
gem 'puma', '~> 6.0' # production-grade web server
gem 'activerecord', '~> 7.0' # if you keep ActiveRecord
gem 'sinatra-activerecord', '~> 2.0' # helpers for ActiveRecord in Sinatra
gem 'rake' # for migrations
group :development do
gem 'sinatra-reloader' # auto‑reload in development
end
# config.ru
require './app'
run Sinatra::Application
# app.rb
require 'sinatra'
require 'sinatra/reloader' if development?
require 'sinatra/activerecord' # optional, only if using AR
class MyApp < Sinatra::Application
# Configuration
set :views, File.join(File.dirname(__FILE__), 'views')
set :public_folder, File.join(File.dirname(__FILE__), 'public')
# Routes will go here
get '/' do
"Hello from Sinatra!"
end
end
You can boot the app with rackup or ruby app.rb. This skeleton gives you a blank canvas. Add any required environment‑specific configuration (database connections, secrets) using the same patterns you'd use in Rails (environment variables, dotenv, YAML files).
Step 2: Extracting Routes and Controllers
Rails controllers are class‑based with actions; Sinatra uses bare blocks (or methods inside a Sinatra::Base subclass). Convert each Rails route into a Sinatra route. For a typical RESTful resource, you'll create multiple get, post, put, delete blocks.
Example: Rails controller for a PostsController might look like:
# Rails
class PostsController < ApplicationController
def index
@posts = Post.all
end
def show
@post = Post.find(params[:id])
end
end
In Sinatra:
# Sinatra routes
get '/posts' do
@posts = Post.all
erb :'posts/index' # renders views/posts/index.erb
end
get '/posts/:id' do
@post = Post.find(params[:id])
erb :'posts/show'
end
If you have many routes, consider splitting them into separate files (e.g., routes/posts.rb) and requiring them. Sinatra allows you to use namespace from sinatra-contrib to group routes:
# Using sinatra-contrib namespace
require 'sinatra/namespace'
namespace '/posts' do
get do
# index
end
get '/:id' do
# show
end
end
Move any before/after filters from Rails (like before_action :authenticate_user!) into Sinatra before blocks. For example:
before '/admin/*' do
redirect '/login' unless logged_in?
end
Step 3: Migrating Models and Database Logic
If you want to keep ActiveRecord (the easiest transition), use the sinatra-activerecord gem. It provides Rake tasks for migrations and a connection setup. Place your models in a models/ folder and require them.
# app.rb
require 'sinatra/activerecord'
# Database configuration
set :database, { adapter: "postgresql", database: "myapp_production", ... }
# Or use a database.yml file:
# set :database_file, 'config/database.yml'
# models/post.rb
class Post < ActiveRecord::Base
validates :title, presence: true
belongs_to :user
end
Run migrations with the same Rake tasks:
# Rakefile
require 'sinatra/activerecord/rake'
require './app'
Alternatively, if ActiveRecord feels too heavy, switch to Sequel or ROM. Sequel integrates smoothly with Sinatra. Example setup:
# Gemfile addition
gem 'sequel'
gem 'pg' # or sqlite3
# app.rb
require 'sequel'
DB = Sequel.connect(ENV['DATABASE_URL'])
# models/post.rb
class Post < Sequel::Model
end
The migration step requires careful handling of callbacks, scopes, and complex queries. Test every model method after porting.
Step 4: Porting Views and Templates
Sinatra uses erb by default, but supports many template engines (Haml, Slim). Views usually live in views/ directory. The layout file defaults to views/layout.erb. Move your Rails layouts and views, adjusting paths. Rails helpers like link_to or form_for are not automatically available; you can either:
- Use plain HTML/Ruby interpolation (keep views minimal).
- Add the
sinatra-helpersgem or write your own lightweight helpers. - Keep using ERB and just replace
render partial:witherb :partial_name, layout: false.
Example partial rendering:
# In a view
<%= erb :'_post_card', layout: false, locals: { post: @post } %>
For layouts, you can define a default layout in views/layout.erb with <%= yield %>. If you need multiple layouts, use the :layout option in erb.
Step 5: Handling Authentication, Sessions, and Middleware
Enable sessions in Sinatra by setting a session secret. Use enable :sessions or set a Rack-based session middleware (e.g., Rack::Session::Cookie).
# app.rb
enable :sessions
set :session_secret, ENV['SESSION_SECRET'] || 'super secret'
If your Rails app uses Devise, migrating directly is challenging because Devise is deeply coupled with Rails. Instead, re‑implement authentication using:
bcryptfor password hashing.- Warden (Rack middleware) for generalized authentication.
- A lightweight service object that verifies credentials and sets
session[:user_id].
A simple login/logout example:
post '/login' do
user = User.find_by(email: params[:email])
if user && user.authenticate(params[:password])
session[:user_id] = user.id
redirect '/dashboard'
else
@error = "Invalid credentials"
erb :login
end
end
get '/logout' do
session.delete(:user_id)
redirect '/'
end
CSRF protection can be added via rack-protection (included in Sinatra) or by manually including tokens. The sinatra-contrib gem also provides csrf helpers.
Step 6: Assets, JavaScript, and CSS
Rails' asset pipeline (Sprockets) is optional. You can serve static files from the public/ directory directly. For more advanced needs (compiling SCSS, CoffeeScript, bundling), use sinatra-assetpack or integrate Sprockets manually.
Basic static file serving is automatic: any file in public/ is served at the root URL. For example, public/css/main.css is accessible at /css/main.css.
If you need Sprockets:
# Gemfile
gem 'sprockets'
gem 'sprockets-sinatra'
# app.rb
require 'sprockets'
require 'sprockets-sinatra'
assets = Sprockets::Environment.new
assets.append_path 'assets/javascripts'
assets.append_path 'assets/stylesheets'
set :assets, assets
For modern front‑end builds, consider decoupling completely: use a separate Node.js build process (Webpack, Vite) that outputs to the public/ directory. Sinatra just serves the final artifacts.
Step 7: Testing and Debugging
Sinatra apps are Rack applications, so testing is straightforward with Rack::Test. Use Minitest or RSpec. Example with RSpec:
# spec/app_spec.rb
require 'rack/test'
require_relative '../app'
RSpec.describe 'My Sinatra App' do
include Rack::Test::Methods
def app
Sinatra::Application
end
it 'loads the home page' do
get '/'
expect(last_response).to be_ok
end
end
Debugging is easier with sinatra-reloader in development. For production‑like debugging, add logging middleware (Rack::CommonLogger) and use structured logging gems.
Best Practices and Common Pitfalls
- Keep it simple: Don't bring over unnecessary abstractions. If a feature was never used in Rails, drop it.
- Organize code early: Use separate files for routes, models, and helpers. A flat
app.rbbecomes messy quickly. - Use Rack middleware wisely: Leverage the Rack ecosystem for caching, throttling, and security instead of re‑implementing inside Sinatra.
- Configuration management: Use environment variables and a dedicated config file (like
config/environment.rb) for different environments. - Database connection pooling: Ensure your ORM uses connection pools (ActiveRecord does by default with Puma, Sequel uses connection pool as well).
- Avoid the God object: Don't stuff all logic into the Sinatra application class. Extract service objects and plain Ruby modules.
- Testing is your safety net: Write integration tests for every ported route before deleting the Rails counterpart.
- Deployment: A Sinatra app deploys just like any Rack app — use Puma behind Nginx, or a modern cloud service. Startup times drop dramatically.
Conclusion
Migrating from Ruby on Rails to Sinatra is a deliberate simplification journey. It forces you to reevaluate every dependency, shedding weight and gaining clarity. The result is a fast, focused application that is easier to deploy, debug, and reason about. While it requires upfront effort — rewriting routes, re‑thinking authentication, and leaving behind Rails' magic — the payoff is a codebase that aligns perfectly with your actual needs. By following the steps above, keeping best practices in mind, and moving incrementally (even running both apps behind a router during transition), you can successfully retire the monolith and embrace a leaner, more agile Sinatra‑powered service.