Sinatra Patterns Skill
Tier 1: Quick Reference
Common Routing Patterns
Basic Routes:
get '/' do 'Hello World' end
post '/users' do
Create user
end
put '/users/:id' do
Update user
end
delete '/users/:id' do
Delete user
end
Route Parameters:
Named parameters
get '/users/:id' do User.find(params[:id]) end
Parameter constraints
get '/users/:id', :id => /\d+/ do
Only matches numeric IDs
end
Wildcard
get '/files/.' do
params['splat'] contains matched segments
end
Query Parameters:
get '/search' do query = params[:q] page = params[:page] || 1 results = search(query, page: page) end
Basic Middleware
Session middleware
use Rack::Session::Cookie, secret: ENV['SESSION_SECRET']
Security middleware
use Rack::Protection
Logging
use Rack::CommonLogger
Compression
use Rack::Deflater
Simple Error Handling
not_found do 'Page not found' end
error do 'Internal server error' end
error 401 do 'Unauthorized' end
Helpers
helpers do def logged_in? !session[:user_id].nil? end
def current_user @current_user ||= User.find_by(id: session[:user_id]) end end
Tier 2: Detailed Instructions
Advanced Routing
Modular Applications:
app/controllers/base_controller.rb
class BaseController < Sinatra::Base configure do set :views, Proc.new { File.join(root, '../views') } set :public_folder, Proc.new { File.join(root, '../public') } end
helpers do def json_response(data, status = 200) content_type :json halt status, data.to_json end end end
app/controllers/users_controller.rb
class UsersController < BaseController get '/' do users = User.all json_response(users.map(&:to_hash)) end
get '/:id' do user = User.find(params[:id]) || halt(404) json_response(user.to_hash) end
post '/' do user = User.create(params[:user]) if user.persisted? json_response(user.to_hash, 201) else json_response({ errors: user.errors }, 422) end end end
config.ru
map '/users' do run UsersController end
Namespaces:
require 'sinatra/namespace'
class App < Sinatra::Base register Sinatra::Namespace
namespace '/api' do namespace '/v1' do get '/users' do # GET /api/v1/users end
namespace '/admin' do
before do
authenticate_admin!
end
get '/stats' do
# GET /api/v1/admin/stats
end
end
end
end end
Route Conditions:
User agent condition
get '/', :agent => /iPhone/ do
Mobile version
end
Custom conditions
set(:auth) do |role| condition do unless current_user && current_user.has_role?(role) halt 403 end end end
get '/admin', :auth => :admin do
Only accessible to admins
end
Host-based routing
get '/', :host => 'admin.example.com' do
Admin subdomain
end
Content Negotiation:
get '/users/:id', :provides => [:json, :xml, :html] do user = User.find(params[:id])
case request.accept.first.to_s when 'application/json' json user.to_json when 'application/xml' xml user.to_xml else erb :user, locals: { user: user } end end
Or using provides helper
get '/users/:id' do user = User.find(params[:id])
respond_to do |format| format.json { json user.to_json } format.xml { xml user.to_xml } format.html { erb :user, locals: { user: user } } end end
Middleware Composition
Custom Middleware:
class RequestLogger def initialize(app) @app = app end
def call(env) start_time = Time.now status, headers, body = @app.call(env) duration = Time.now - start_time
puts "#{env['REQUEST_METHOD']} #{env['PATH_INFO']} - #{status} (#{duration}s)"
[status, headers, body]
end end
use RequestLogger
Middleware Ordering:
config.ru
use Rack::Deflater # Compression first use Rack::Static # Static files use Rack::CommonLogger # Logging use Rack::Session::Cookie # Sessions use Rack::Protection # Security use CustomAuthentication # Auth run Application
Template Integration
ERB Templates:
views/layout.erb
<!DOCTYPE html> <html> <head> <title><%= @title || 'My App' %></title> </head> <body> <%= yield %> </body> </html>
views/users/index.erb
<h1>Users</h1> <ul> <% @users.each do |user| %> <li><%= user.name %></li> <% end %> </ul>
Controller
get '/users' do @users = User.all @title = 'User List' erb :'users/index' end
Inline Templates:
get '/' do erb :index end
END
@@layout <!DOCTYPE html> <html> <body><%= yield %></body> </html>
@@index <h1>Welcome</h1>
Template Engines:
Haml
get '/' do haml :index end
Slim
get '/' do slim :index end
Liquid (safe for user content)
get '/' do liquid :index, locals: { user: current_user } end
Error Handling Patterns
Comprehensive Error Handling:
class Application < Sinatra::Base
Development configuration
configure :development do set :show_exceptions, :after_handler set :dump_errors, true end
Production configuration
configure :production do set :show_exceptions, false set :dump_errors, false end
Specific exception handlers
error ActiveRecord::RecordNotFound do status 404 json({ error: 'Resource not found' }) end
error ActiveRecord::RecordInvalid do status 422 json({ error: 'Validation failed', details: env['sinatra.error'].message }) end
error Sequel::NoMatchingRow do status 404 json({ error: 'Not found' }) end
HTTP status handlers
not_found do json({ error: 'Endpoint not found' }) end
error 401 do json({ error: 'Unauthorized' }) end
error 403 do json({ error: 'Forbidden' }) end
error 422 do json({ error: 'Unprocessable entity' }) end
Catch-all error handler
error do error = env['sinatra.error'] logger.error("Error: #{error.message}") logger.error(error.backtrace.join("\n"))
status 500
json({ error: 'Internal server error' })
end end
Before/After Filters
Request Filters:
Global before filter
before do content_type :json end
Path-specific filters
before '/admin/*' do authenticate_admin! end
Conditional filters
before do pass unless request.path.start_with?('/api') authenticate_api_user! end
After filters
after do
Add CORS headers
headers 'Access-Control-Allow-Origin' => '*' end
Modify response
after do response.body = response.body.map(&:upcase) if params[:uppercase] end
Session Management
Cookie Sessions:
use Rack::Session::Cookie, key: 'app.session', secret: ENV['SESSION_SECRET'], expire_after: 86400, # 1 day secure: production?, httponly: true, same_site: :strict
helpers do def login(user) session[:user_id] = user.id session[:logged_in_at] = Time.now.to_i end
def logout session.clear end
def current_user return nil unless session[:user_id] @current_user ||= User.find_by(id: session[:user_id]) end end
Tier 3: Resources & Examples
Full Application Example
See assets/modular-app-template/ for complete modular application structure.
Performance Patterns
Caching:
HTTP caching
get '/public/data' do cache_control :public, max_age: 3600 etag calculate_etag last_modified last_update_time
json PublicData.all.map(&:to_hash) end
Fragment caching with Redis
require 'redis'
helpers do def cache_fetch(key, expires_in: 300, &block) cached = REDIS.get(key) return JSON.parse(cached) if cached
data = block.call
REDIS.setex(key, expires_in, data.to_json)
data
end end
get '/expensive-data' do data = cache_fetch('expensive-data', expires_in: 600) do perform_expensive_query end
json data end
Streaming Responses:
Stream large responses
get '/large-export' do stream do |out| User.find_each do |user| out << user.to_csv_row end end end
Server-Sent Events
get '/events', provides: 'text/event-stream' do stream :keep_open do |out| EventSource.subscribe do |event| out << "data: #{event.to_json}\n\n" end end end
Production Configuration
Complete config.ru:
config.ru
require_relative 'config/environment'
Production middleware
if ENV['RACK_ENV'] == 'production' use Rack::SSL use Rack::Deflater end
Static files
use Rack::Static, urls: ['/css', '/js', '/images'], root: 'public', header_rules: [ [:all, {'Cache-Control' => 'public, max-age=31536000'}] ]
Logging
use Rack::CommonLogger
Sessions
use Rack::Session::Cookie, secret: ENV['SESSION_SECRET'], same_site: :strict, httponly: true, secure: ENV['RACK_ENV'] == 'production'
Security
use Rack::Protection, except: [:session_hijacking], use: :all
Rate limiting (production)
if ENV['RACK_ENV'] == 'production' require 'rack/attack' use Rack::Attack end
Mount applications
map '/api/v1' do run ApiV1::Application end
map '/' do run WebApplication end
Rack::Attack Configuration:
config/rack_attack.rb
class Rack::Attack
Throttle login attempts
throttle('login/ip', limit: 5, period: 60) do |req| req.ip if req.path == '/login' && req.post? end
Throttle API requests
throttle('api/ip', limit: 100, period: 60) do |req| req.ip if req.path.start_with?('/api') end
Block suspicious requests
blocklist('block bad user agents') do |req| req.user_agent =~ /bad_bot/i end end
Testing Patterns
See references/testing-examples.rb for comprehensive test patterns.
Project Structure
Recommended modular structure:
app/ controllers/ base_controller.rb api_controller.rb users_controller.rb posts_controller.rb models/ user.rb post.rb services/ user_service.rb authentication_service.rb helpers/ application_helpers.rb view_helpers.rb config/ environment.rb database.yml puma.rb db/ migrations/ lib/ middleware/ custom_auth.rb tasks/ public/ css/ js/ images/ views/ layout.erb users/ index.erb show.erb spec/ controllers/ models/ spec_helper.rb config.ru Gemfile Rakefile README.md
Additional Resources
-
Routing Examples: assets/routing-examples.rb
-
Middleware Patterns: assets/middleware-patterns.rb
-
Modular App Template: assets/modular-app-template/
-
Production Config: references/production-config.rb
-
Testing Guide: references/testing-examples.rb