Files
gh-geoffjay-claude-plugins-…/skills/sinatra-patterns/SKILL.md
2025-11-29 18:28:07 +08:00

657 lines
11 KiB
Markdown

---
name: sinatra-patterns
description: Common Sinatra patterns, routing strategies, error handling, and application organization. Use when building Sinatra applications or designing routes.
---
# Sinatra Patterns Skill
## Tier 1: Quick Reference
### Common Routing Patterns
**Basic Routes:**
```ruby
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:**
```ruby
# 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:**
```ruby
get '/search' do
query = params[:q]
page = params[:page] || 1
results = search(query, page: page)
end
```
### Basic Middleware
```ruby
# 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
```ruby
not_found do
'Page not found'
end
error do
'Internal server error'
end
error 401 do
'Unauthorized'
end
```
### Helpers
```ruby
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:**
```ruby
# 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:**
```ruby
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:**
```ruby
# 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:**
```ruby
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:**
```ruby
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:**
```ruby
# 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:**
```ruby
# 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:**
```ruby
get '/' do
erb :index
end
__END__
@@layout
<!DOCTYPE html>
<html>
<body><%= yield %></body>
</html>
@@index
<h1>Welcome</h1>
```
**Template Engines:**
```ruby
# 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:**
```ruby
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:**
```ruby
# 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:**
```ruby
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:**
```ruby
# 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:**
```ruby
# 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:**
```ruby
# 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:**
```ruby
# 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`