01 · Building Web Apps with Sinatra¶
Every Ruby web framework you've heard of — including Rails — is built on Rack, a simple interface between web servers and Ruby applications. Sinatra is the thinnest possible layer on top of Rack: define a route, write a block, return a string. No controllers, no folders, no convention-over-configuration magic. That makes it the fastest way to see how HTTP actually maps onto Ruby code.
Your first Sinatra app¶
Running ruby app.rb starts a server on port 4567 by default (WEBrick or
Puma, whichever is installed). Visiting / in a browser runs the block
and sends its return value back as the response body — in Sinatra, the
last expression in a route block is the response, exactly like a method.
Routes and dynamic segments¶
Routes are declared with an HTTP verb method (get, post, put,
patch, delete) and a path. A path segment starting with : becomes a
named parameter, available through the params hash:
A request to /greet/Ruby runs the block with params[:name] == "Ruby"
and returns "Hello, Ruby!".
Reading form and query data¶
params merges route segments, query-string parameters, and submitted
form fields into a single hash — you don't need to distinguish where a
value came from to read it:
A POST /echo with body message=hi+there returns "You sent: hi
there". A GET /echo?message=hi would land in the same params[:message]
if you also defined a get '/echo' route.
Testing routes without a real server¶
You rarely want to boot an actual HTTP server to check a route. rack-test
drives your app in-process by sending fake Rack requests directly to it,
which is both faster and deterministic:
# test_app.rb
require 'rack/test'
require_relative 'app'
class Tester
include Rack::Test::Methods
def app; Sinatra::Application; end
end
t = Tester.new
t.get '/'
puts t.last_response.status # 200
puts t.last_response.body # Hello, Sinatra!
t.get '/greet/Ruby'
puts t.last_response.body # Hello, Ruby!
t.post '/echo', message: 'hi there'
puts t.last_response.body # You sent: hi there
Captured output from running ruby test_app.rb:
Views with ERB¶
Returning raw strings works for a demo, but real apps render templates.
Sinatra looks for view files in a views/ directory next to your app
file and renders them with erb:
<%= %> interpolates a Ruby expression into the output; plain <% %>
runs Ruby without printing anything (useful for if/each blocks).
Before filters and halting¶
A before block runs ahead of every matching route — the classic place
to check authentication or set shared state:
before '/admin/*' do
halt 401, "Not authorized" unless params[:token] == "secret"
end
get '/admin/dashboard' do
"Welcome, admin"
end
halt immediately stops processing and sends the given status and body —
the route block below never runs if the before filter halts first.
Sinatra-specific traps¶
- Route order matters. Sinatra matches routes top-to-bottom and stops
at the first match. A generic
get '/:id'declared beforeget '/new'will swallow requests meant for/new(params[:id]would be"new"). Put specific routes first. paramskeys can be strings or symbols depending on source. Route-segment params are always symbols, but Sinatra also indifferently duplicates them as strings in some setups. Don't assume; useparams[:name]consistently rather than mixingparams["name"].- The classic single-file style (
require 'sinatra') is process-global — there's only oneSinatra::Application. For multiple independent apps in one process, use the modular style (class App < Sinatra::Base) instead, which is also whatrack-testneeds to target more than one app. haltinside a block silently ends the block — code after a triggeredhaltnever executes. It's easy to forget this isn't justreturn; it also skips anyafterfilters' assumptions about reaching a normal response.- Sinatra 4.x enables
host_authorizationby default, which rejects requests whoseHostheader isn't recognized — including requests fromrack-testunless you setset :host_authorization, { permitted_hosts: [] }(fine for tests/dev, tighten it for real deployments).
How It Actually Works¶
Sinatra is not a separate server — it's a thin DSL over Rack, the
common interface nearly every Ruby web server and framework (including
Rails) speaks: a Rack app is any object responding to #call(env) that
returns a [status, headers, body] triple. get "/path" do ... end
registers a route by storing the pattern and block in an internal array;
on each incoming request, Sinatra's own #call method (which is the Rack
entry point) walks that array top-to-bottom looking for the first pattern
that matches the request path and method, then evaluates your block in the
context of a per-request instance — which is why every route handler has
access to params, request, and instance variables scoped to just that
one request, with no cross-request leakage. Under something like
rackup/Puma, each incoming connection typically gets its own thread (or
process, in a forking server), so two simultaneous requests to the same
Sinatra app really are handled by two different self instances even
though they share the same loaded class and route table.
Cheat sheet¶
| Task | Sinatra code |
|---|---|
| Define a GET route | get('/path') { ... } |
| Named URL parameter | get('/x/:id') { params[:id] } |
| Read form/query param | params[:field] |
| Render a template | erb :template_name |
| Run before every route | before { ... } |
| Stop early with a status | halt 404, "Not found" |
| Redirect | redirect '/somewhere' |
| Set a response header | headers['X-Custom'] = 'value' |
| Modular app base class | class App < Sinatra::Base |
| Test in-process | include Rack::Test::Methods |
Exercise¶
Build a tiny Sinatra "notes" app in a single app.rb:
- An in-memory
NOTES = []array of hashes ({ id:, text: }). GET /notesreturns all notes joined by newlines (empty array →"No notes yet").POST /notesreadsparams[:text], appends a new note with an auto-incrementingid, and returns"Added note #\#{id}".GET /notes/:idreturns the matching note's text, or halts with404, "Not found"if no note has that id.- Write a
rack-testscript that posts two notes, then fetches both/notesand/notes/:idfor each, printing the responses.
Run your test script and confirm the printed output matches what you'd expect from the routes you wrote.