I've built APIs with all three of these frameworks. Each time I start a new project, I face the same question: Rails for the ecosystem, Sinatra for simplicity, or Roda for performance?
After shipping APIs that handle everything from simple webhooks to high-traffic marketplace backends, I've learned the "best" framework depends on what you're building.
Let me show you real code, actual benchmarks, and the trade-offs that matter in production.
The Three Frameworks
Rails API - Full-featured. Opinionated. Ecosystem that solves almost every problem.
Sinatra - Minimalist. Flexible. Perfect when you want to stay close to HTTP.
Roda - Fast. Tree-structured routing. Plugin system gives you exactly what you need.
Same API, Three Ways
Let's build identical user management APIs to see how they compare.
Rails API
# Gemfile
gem 'rails', '~> 7.1'
gem 'sqlite3'
gem 'bcrypt'
# app/models/user.rb
class User < ApplicationRecord
has_secure_password
validates :email, presence: true, uniqueness: true
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
include ActionController::MimeResponds
end
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def index
@users = User.all
render json: @users
end
def create
@user = User.new(user_params)
if @user.save
render json: @user, status: :created
else
render json: @user.errors, status: :unprocessable_entity
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password)
end
end
# config/routes.rb
Rails.application.routes.draw do
resources :users
endEverything you expect from Rails. ActiveRecord, strong parameters, built-in validations.
Sinatra
# Gemfile
gem 'sinatra'
gem 'sinatra-activerecord'
gem 'sqlite3'
gem 'bcrypt'
gem 'json'
# app.rb
require 'sinatra'
require 'sinatra/activerecord'
require 'bcrypt'
require 'json'
set :database, { adapter: 'sqlite3', database: 'users.db' }
class User < ActiveRecord::Base
validates :email, presence: true, uniqueness: true
end
# Error handling
error ActiveRecord::RecordNotFound do
content_type :json
status 404
{ error: 'Not found' }.to_json
end
error ActiveRecord::RecordInvalid do |e|
content_type :json
status 422
{ error: 'Validation failed', details: e.record.errors.full_messages }.to_json
end
# Routes
get '/users' do
content_type :json
User.all.to_json
end
get '/users/:id' do
content_type :json
user = User.find(params[:id])
user.to_json
end
post '/users' do
content_type :json
data = JSON.parse(request.body.read)
user = User.new(data)
user.save!
status 201
user.to_json
end
put '/users/:id' do
content_type :json
user = User.find(params[:id])
data = JSON.parse(request.body.read)
user.update!(data)
user.to_json
end
delete '/users/:id' do
user = User.find(params[:id])
user.destroy!
status 204
endClean. Direct. You see exactly what's happening.
Roda
# Gemfile
gem 'roda'
gem 'sequel'
gem 'sqlite3'
gem 'bcrypt'
gem 'json'
# app.rb
require 'roda'
require 'sequel'
require 'bcrypt'
require 'json'
DB = Sequel.sqlite('users.db')
DB.create_table? :users do
primary_key :id
String :name, null: false
String :email, null: false, unique: true
String :password_hash, null: false
DateTime :created_at
DateTime :updated_at
end
class User < Sequel::Model
def validate
super
errors.add(:email, 'must be unique') if User.where(email: email).exclude(id: id).any?
end
end
class App < Roda
route do |r|
r.on 'users' do
r.get do
User.all.to_json
end
r.post do
data = JSON.parse(request.body.read)
user = User.create(data)
[201, user.to_json]
end
r.on Integer do |id|
user = User[id] || (r.get { [404, 'Not found'] })
r.get do
user.to_json
end
r.put do
data = JSON.parse(request.body.read)
user.update(data)
user.to_json
end
r.delete do
user.delete
[204]
end
end
end
end
endTree routing looks weird at first. Then it clicks. Each level handles its scope.
Performance Numbers
Tested on MacBook Pro M1, 16GB RAM. Apache Bench, 1000 requests, 10 concurrent connections.
GET /users
- Rails API: 847 req/sec
- Sinatra: 1,247 req/sec
- Roda: 2,134 req/sec
POST /users (with validation)
- Rails API: 423 req/sec
- Sinatra: 756 req/sec
- Roda: 1,289 req/sec
Memory (RSS after 1000 requests)
- Rails API: 84MB
- Sinatra: 32MB
- Roda: 28MB
Roda wins on speed. Not by a little. 2-3x faster than Rails.
Does it matter for your 1,000-user MVP? Probably not. For 100,000 concurrent users? Yeah.
What It's Actually Like
Rails API: Batteries Included
The good:
Everything you know from Rails works. ActiveRecord migrations. Strong parameters. Built-in testing. Background jobs with ActiveJob.
When I built an e-commerce API last year, Rails saved weeks. Need auth? Devise. File uploads? ActiveStorage. Background jobs? ActiveJob works with any queue.
The annoying:
Memory footprint is real. Our Rails API containers start at 200-300MB before handling traffic. For simple APIs, that's wasteful.
Boot time matters. Rails takes 3-5 seconds to start in development. Adds up when running tests constantly.
Sinatra: HTTP with a Ruby Wrapper
The good:
Feels like HTTP. Routing is intuitive. Never confused about what's happening behind the scenes.
I used Sinatra for a webhook receiver handling 20 different formats. The flexibility to handle each route exactly how I wanted, without fighting framework conventions, was perfect.
The annoying:
You're on your own. Need parameter validation? Find a gem or write it. Want background jobs? Pick from a dozen options and wire it up.
Beyond simple APIs, you rebuild patterns Rails gives for free.
Roda: Performance First
The good:
Sinatra's performance-focused cousin. Tree routing is intuitive once you get it. Plugin system adds exactly what you need.
I built a high-traffic analytics API with Roda handling 50,000+ requests daily. The performance headroom gave us confidence to scale without worrying about framework overhead.
The annoying:
Smaller ecosystem. Roda has plugins for most needs, but sometimes you adapt Rack middleware or build it yourself.
Documentation is good, not great. You'll read source code to understand advanced features.
When to Use What
Use Rails When
Building complex business apps - User management, payments, file uploads, background jobs, email. Rails has battle-tested solutions for all of it.
Your team knows Rails - Productivity from familiar patterns usually beats performance gains. Developer time costs more than servers.
Rapid prototyping - Rails scaffolding gets you from idea to working API faster than anything else.
Real example: I used Rails API for a SaaS platform needing auth, subscription billing, file processing, and admin dashboards. The gem ecosystem saved months.
Use Sinatra When
Building microservices - Small, focused services doing one thing well. Sinatra's simplicity is perfect.
Maximum flexibility - When your API has unusual requirements that don't fit REST patterns, Sinatra gets out of the way.
Integrating existing systems - Easy to wrap legacy systems or create adapter APIs without imposing structure.
Real example: I built a webhook proxy receiving webhooks from 15 services and translating them to unified format. Flexibility to handle each service's quirks was essential.
Use Roda When
Performance is critical - High-traffic APIs where every millisecond matters. Roda's speed advantage is significant.
Structure without bloat - More organization than Sinatra, less overhead than Rails.
JSON APIs - Roda's plugin system is particularly well-suited for API-only apps.
Real example: I used Roda for real-time chat API handling thousands of concurrent WebSocket connections. Low memory footprint let us run more instances per server.
Authentication Example
Here's JWT auth in Sinatra:
require 'jwt'
SECRET_KEY = ENV['SECRET_KEY'] || 'your-secret-key'
def authenticate_user
token = request.env['HTTP_AUTHORIZATION']&.split(' ')&.last
halt 401, { error: 'Missing token' }.to_json unless token
begin
decoded = JWT.decode(token, SECRET_KEY, true, { algorithm: 'HS256' })
@current_user = User.find(decoded[0]['user_id'])
rescue JWT::DecodeError, ActiveRecord::RecordNotFound
halt 401, { error: 'Invalid token' }.to_json
end
end
before do
authenticate_user unless request.path_info == '/login'
content_type :json
end
post '/login' do
data = JSON.parse(request.body.read)
user = User.find_by(email: data['email'])
if user&.authenticate(data['password'])
token = JWT.encode(
{ user_id: user.id, exp: Time.now.to_i + 24*60*60 },
SECRET_KEY,
'HS256'
)
{ token: token, user: user.to_hash }.to_json
else
status 401
{ error: 'Invalid credentials' }.to_json
end
endStraightforward. No magic.
Testing
Sinatra and Roda use Rack::Test. Simple:
require 'minitest/autorun'
require 'rack/test'
require_relative 'app'
class AppTest < Minitest::Test
include Rack::Test::Methods
def app
Sinatra::Application
end
def test_get_users
get '/users'
assert_equal 200, last_response.status
users = JSON.parse(last_response.body)
assert_kind_of Array, users
end
def test_create_user
post '/users', JSON.dump({ name: 'John', email: 'john@example.com' }),
{ 'CONTENT_TYPE' => 'application/json' }
assert_equal 201, last_response.status
end
endRails has more built-in test helpers. Depends on what you prefer.
Production Numbers
Based on my deployments:
Memory per Worker
- Rails API: Starts at 200MB, grows to 300-400MB. Budget 512MB per worker.
- Sinatra: Starts at 50MB, grows to 80-120MB. Budget 256MB per worker.
- Roda: Starts at 30MB, grows to 60-100MB. Budget 128MB per worker.
Cold Start Times
For serverless or auto-scaling:
- Rails API: 2-4 seconds
- Sinatra: 0.5-1 second
- Roda: 0.3-0.8 seconds
Ecosystem
- Rails: Works with virtually every Ruby gem. ActiveRecord integrations are seamless.
- Sinatra: Most gems work, but you handle integration. Some Rails-specific gems won't work.
- Roda: Works with most gems. Prefer Sequel over ActiveRecord. Some manual integration needed.
Learning Curve
- Rails: Steep at first. Incredible docs and community. Rails Guides are exceptional.
- Sinatra: Gentle curve. Good docs, not comprehensive. Lots of examples online.
- Roda: Moderate curve. Good docs, smaller community. You'll read source more.
What I Actually Choose
After building production APIs with all three:
- New teams or complex apps: Rails API. Productivity and ecosystem usually beat performance. Optimize later.
- Experienced Ruby teams with focused APIs: Roda. Performance and flexibility are worth the smaller ecosystem.
- Microservices or specialized services: Sinatra. Simple and well-understood.
- High-traffic, performance-critical APIs: Roda. Speed difference matters at scale.
Switching Later
It's not hard to switch if you structure code well.
Keep business logic in plain Ruby objects (service objects, domain models). Treat framework as thin HTTP layer.
I've migrated APIs from Sinatra to Rails and Rails to Roda with minimal changes to core logic.
Bottom Line
No universally "best" framework. I've shipped successful APIs with all three:
- Rails when ecosystem and productivity matter most
- Sinatra when simplicity and flexibility are key
- Roda when performance and structure are both important
Start with what your team knows. Optimize when you have real performance problems, not imaginary ones.
Focus on building something people want to use. That matters way more than framework choice.