| name | sinatra |
| description | Sinatra framework guardrails, patterns, and best practices for AI-assisted development.
Use when working with Sinatra projects, or when the user mentions Sinatra.
Provides lightweight Ruby web patterns, routing DSL, middleware, and REST API guidelines.
|
| license | MIT |
| metadata | {"author":"samuel","version":"1.0","category":"framework","language":"ruby","extensions":".rb"} |
Sinatra Framework Guide
Applies to: Sinatra 3.x, Ruby 3.0+, REST APIs, Microservices, Prototypes
Language Guide: @.claude/skills/ruby-guide/SKILL.md
Overview
Sinatra is a lightweight DSL for building web applications and APIs in Ruby. It maps HTTP verbs directly to Ruby blocks, providing minimal ceremony and maximum flexibility.
Use Sinatra when:
- Building microservices or small-to-medium APIs
- Rapid prototyping or proof-of-concept work
- Lightweight webhooks, proxies, or internal tools
- You want minimal framework overhead
Consider alternatives when:
- You need a full MVC framework (use Rails)
- You need built-in admin, ORM, and auth out of the box (use Rails)
- Application complexity exceeds 10+ route modules (use Rails or Hanami)
Guardrails
Sinatra-Specific Guidelines
- Use modular style (
Sinatra::Base) for production applications
- Classic style is acceptable for scripts and prototypes only
- Use
sinatra-contrib for JSON, namespaces, and reloader support
- Use Puma as the production application server
- Use Bundler for dependency management with a Gemfile
- Separate routes into modules via
register and Sinatra::Namespace
- Extract helpers into dedicated modules under
app/helpers/
- Use service objects for business logic (keep routes thin)
Security Guidelines
- Never hardcode secrets; use environment variables via
dotenv
- Set
session_secret from ENV (not random fallback in production)
- Validate all user input before processing
- Use parameterized queries via ActiveRecord or Sequel ORM
- Implement authentication checks with
before filters
- Use HTTPS in production with proper HSTS headers
- Implement rate limiting via Rack middleware
- Configure CORS restrictively (never
* in production)
Testing Guidelines
- Use RSpec with
rack-test for HTTP integration tests
- Use
database_cleaner to reset state between tests
- Test both success and error paths for every endpoint
- Mock external services (never call real APIs in tests)
- Use factory_bot for test data setup
- Coverage target: >80% for business logic
Project Structure
Simple Application (Prototypes Only)
myapp/
├── app.rb # All routes and config
├── config.ru # Rack configuration
├── Gemfile
├── public/ # Static assets
├── views/ # ERB/Haml templates
│ ├── layout.erb
│ └── index.erb
└── spec/
└── app_spec.rb
Modular Application (Production)
myapp/
├── config.ru # Rack entry point
├── Gemfile
├── Rakefile # Database tasks
├── app/
│ ├── main.rb # Sinatra::Base application class
│ ├── routes/ # Route modules (one per resource)
│ │ ├── auth.rb
│ │ ├── users.rb
│ │ └── posts.rb
│ ├── models/ # ActiveRecord/Sequel models
│ │ ├── user.rb
│ │ └── post.rb
│ ├── services/ # Business logic (no Sinatra imports)
│ │ └── user_service.rb
│ └── helpers/ # Reusable helper modules
│ ├── auth_helper.rb
│ └── response_helper.rb
├── config/
│ ├── database.yml
│ └── environment.rb # Boot: dotenv, bundler, DB setup
├── db/
│ └── migrations/
├── public/
├── views/
└── spec/
├── spec_helper.rb
├── routes/
└── models/
Key conventions:
app/main.rb contains the Sinatra::Base subclass and wires everything together
app/routes/ holds one module per resource, registered via register
app/services/ holds business logic with no framework coupling
app/helpers/ holds modules included via helpers DSL
config/environment.rb boots dotenv, bundler, and database connection
Routing DSL
Sinatra maps HTTP verbs directly to Ruby blocks. Parameters are captured via :name symbols.
get '/users' { json users: User.all.map(&:to_h) }
get '/users/:id' { json user: find_user!.to_h }
post '/users' { create_user }
put '/users/:id' { update_user }
delete '/users/:id' { delete_user }
Route Parameters
get '/users/:id' do
user = User.find(params[:id])
json user: user.to_h
end
get '/files/*.*' do
end
get '/search' do
end
get %r{/posts/(\d+)} do |id|
Post.find(id)
end
Namespaced Routes
Use sinatra/namespace to group routes with shared prefixes.
require 'sinatra/namespace'
class App < Sinatra::Base
register Sinatra::Namespace
namespace '/api/v1' do
namespace '/users' do
get { json users: User.all.map(&:to_h) }
post { create_user }
get '/:id' do
json user: User.find(params[:id]).to_h
end
end
end
end
Request & Response
Reading Request Data
def json_params
@json_params ||= begin
body = request.body.read
body.empty? ? {} : JSON.parse(body, symbolize_names: true)
end
rescue JSON::ParserError
halt 400, json(error: 'Invalid JSON')
end
params[:field_name]
request.env['HTTP_AUTHORIZATION']
request.content_type
request.accept?('application/json')
Response Patterns
get '/health' do
json status: 'ok', timestamp: Time.now.iso8601
end
post '/users' do
user = User.create!(json_params)
status 201
json user: user.to_h
end
halt 404, json(error: 'Not found')
halt 401, json(error: 'Unauthorized')
halt 422, json(errors: user.errors.full_messages)
redirect '/login'
redirect '/users', 303
headers 'X-Custom-Header' => 'value'
content_type :json
Helpers
Define reusable methods available in routes and views.
module AuthHelper
def current_user
@current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
end
def require_login!
halt 401, json(error: 'Unauthorized') unless current_user
end
def require_admin!
require_login!
halt 403, json(error: 'Forbidden') unless current_user.admin?
end
end
module ResponseHelper
def paginate(collection, per_page: 20)
page = (params[:page] || 1).to_i
per = (params[:per_page] || per_page).to_i
total = collection.count
items = collection.offset((page - 1) * per).limit(per)
{ items: items, meta: { page: page, per_page: per, total: total } }
end
end
class App <
helpers
helpers
Views & Templates
Sinatra supports ERB, Haml, Slim, and other Tilt-compatible template engines. For JSON APIs, skip views entirely and use the json helper.
get '/' do
@title = 'Home'
erb :index
end
get '/admin' do
erb :dashboard, layout: :admin_layout
end
Use template inheritance via <%= yield %> in views/layout.erb. Keep logic out of templates; use helpers and instance variables set in routes.
Error Handling
Error Handlers
class App < Sinatra::Base
not_found do
json error: 'Not found'
end
error do
json error: 'Internal server error'
end
error ActiveRecord::RecordNotFound do
status 404
json error: 'Resource not found'
end
error ActiveRecord::RecordInvalid do |e|
status 422
json errors: e.record.errors.full_messages
end
error JSON::ParserError do
status 400
json error: 'Invalid JSON'
end
end
Environment-Specific Error Display
configure :development do
enable :show_exceptions
enable :logging
end
configure :production do
disable :show_exceptions
enable :raise_errors
end
Filters
Use before and after filters for cross-cutting concerns.
class App < Sinatra::Base
before do
content_type :json if request.accept?('application/json')
end
before '/api/*' do
content_type :json
authenticate_token!
end
after do
response.headers['X-Request-Id'] = SecureRandom.uuid
end
end
Modular Application Pattern
Application Class (app/main.rb)
require 'sinatra/base'
require 'sinatra/json'
require 'sinatra/namespace'
class App < Sinatra::Base
register Sinatra::Namespace
configure do
set :server, :puma
set :root, File.dirname(__FILE__)
set :views, proc { File.join(root, '..', 'views') }
enable :sessions
set :session_secret, ENV.fetch('SESSION_SECRET')
end
configure :development do
require 'sinatra/reloader'
register Sinatra::Reloader
enable :logging
end
Dir[File.join(__dir__, 'helpers', '*.rb')].each { |f| require f }
helpers AuthHelper, ResponseHelper
Dir[File.join(__dir__, 'routes', '*.rb')].each { |f| require f }
register
register
get() { json }
not_found { json }
error { json }
Route Module (app/routes/users.rb)
module Routes
module Users
def self.registered(app)
app.namespace '/api/v1/users' do
get { json users: User.all.map(&:to_h) }
get('/:id') { json user: User.find(params[:id]).to_h }
post do
user = User.create!(json_params)
status 201
json user: user.to_h
rescue ActiveRecord::RecordInvalid => e
status 422
json errors: e.record.errors.full_messages
end
end
end
end
end
Rack Configuration (config.ru)
require 'bundler/setup'
Bundler.require(:default, ENV.fetch('RACK_ENV', 'development'))
require_relative 'config/environment'
require_relative 'app/main'
run App
Commands Reference
bundle install
bundle exec ruby app.rb
bundle exec rackup -p 4567
bundle exec rspec
bundle exec rspec --format documentation
bundle exec rspec --format progress --require simplecov
bundle exec rake db:create
bundle exec rake db:migrate
bundle exec rake db:rollback
bundle exec puma -C config/puma.rb
bundle exec pry -r ./config/environment
docker build -t myapp .
docker run -p 4567:4567 myapp
Dependencies
| Gem | Purpose |
|---|
sinatra | Web framework DSL |
sinatra-contrib | Extensions: JSON, namespace, reloader |
puma | Production application server |
rake | Task runner for migrations |
dotenv | Environment variable loading |
activerecord / sequel | ORM for database access |
pg | PostgreSQL adapter |
bcrypt | Password hashing (has_secure_password) |
jwt | JSON Web Token authentication |
rspec | Test framework |
rack-test | HTTP request helpers for Rack apps |
database_cleaner-active_record | DB cleanup between tests |
factory_bot | Test data factories |
Advanced Topics
For detailed code examples and advanced patterns, see:
- references/patterns.md -- Database integration, authentication (sessions and JWT), Rack middleware, testing patterns, deployment, and Sequel ORM alternative
External References