← Back to DevBytes

Migrating from Ruby on Rails to Sinatra: Step-by-Step Guide

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:

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:

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:

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

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.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles