skip to content

Sinatra

Sinatra turns route blocks such as get '/' into a Rack app, with filters, helpers and halt for flow control. Interviewers use it as the contrast to Rails when sizing a small service.

on this pageshow

questions

6

In Sinatra, how is a request matched to a route such as get '/payments/:id', and how do you read path, splat and query parameters?

level: juniorimportance: must knowfreq 68%

answer

  1. one list per verb, definition order
  2. first match runs, the rest never do
  3. params takes string or symbol keys
  4. splat array, captures for regexps
  5. strict_paths is true by default

basics

~20 s

Sinatra tries the routes for the request's verb in definition order and runs the first match. Named segments, splats, regexp captures, query and form fields all land in params as strings, readable by string or symbol key.

solid answer

~50 s

Each `get`, `post` or other verb call compiles its path into a Mustermann pattern and appends it to that verb's list; `get` also registers the same route for `HEAD`. For a request, Sinatra walks the list in definition order and runs the first route whose pattern matches `path_info` and whose conditions pass; there is no ranking by specificity. `:id` captures one segment into `params['id']`, `*` fills the array `params['splat']`, a regexp route fills `params['captures']`, and a trailing `?` makes a segment optional. Query-string and form fields are merged into the same `params`, a `Sinatra::IndifferentHash`, so `params[:id]` and `params['id']` agree, and a path parameter wins over a query field of the same name. Values are Strings, and splat and captures are Arrays of Strings. Paths are strict: `/payments` does not match `/payments/` unless you `disable :strict_paths`.

code

ruby · 16 lines
ruby
require 'sinatra'

post '/webhooks/:provider' do
  provider = params['provider']          # path segment, a String
  mode     = params['mode'] || 'live'    # query string ?mode=test
  "#{provider}:#{mode}"
end

get '/receipts/*.*' do |path, ext|
  "#{path} as #{ext}"   # /receipts/2026/r1.pdf => "2026/r1 as pdf"
end

get '/payments/:id' do
  id = Integer(params[:id], 10)          # symbol key works too
  "payment #{id}"
end

go deeper

for a junior

Recall that routes are verb plus pattern plus block, that the first match in definition order wins, and that params holds every parameter as a string.

for a middle

Explain where each parameter comes from: named segments, splat, captures, query and form fields, the indifferent hash, and which value wins on a name clash.

for a senior

Show how route order, strict_paths and string-typed params cause real bugs, and how conditions, pass or narrower patterns fix overlapping routes.

for a principal

Judge when a growing route list needs structure, such as separate modular apps or namespaces, rather than more carefully ordered patterns in one file.

## What a route is In Sinatra a **route** is an HTTP verb, a path pattern and a block. Writing `get '/payments/:id' do ... end` in an application class (or at the top level of a classic app) does three things: - it compiles the path string into a **Mustermann** pattern object, the pattern library Sinatra depends on; - it turns the block into a method on the application class, so the block later runs with that request's application instance as `self`; - it appends the pattern, its conditions and that method to a per-verb list, `routes['GET']`. `get` is special: it registers the same pattern for `HEAD` too, so a `HEAD` request is answered without a second route. The other verb methods are `post`, `put`, `patch`, `delete`, `options`, `link` and `unlink`. ## How a request picks its route Matching is deliberately simple: 1. Sinatra takes the list for the request's method. 2. It walks that list **in the order the routes were defined**; routes inherited from a superclass are tried after the subclass's own. 3. For each route it matches the pattern against `request.path_info`. If the pattern matches and every **condition** passes (`provides:`, `host_name:`, `agent:` or a custom `condition`), the block runs. 4. The block's return value becomes the response and routing stops. A second matching route runs only if the first calls `pass`. 5. If nothing matches, Sinatra raises `Sinatra::NotFound`, which becomes a 404, unless the app runs as middleware and forwards the request downstream. There is **no "most specific route wins" rule**. With `get '/payments/:id'` defined above `get '/payments/refunds'`, a request for `/payments/refunds` goes to the first route with `params['id'] == 'refunds'`. Define literal routes first, or narrow the pattern. Trailing slashes matter as well. The `strict_paths` setting defaults to `true`, so `/payments` and `/payments/` are different paths; `disable :strict_paths` makes Sinatra strip one trailing slash before matching. ## Conditions narrow a match A pattern match is necessary but not sufficient. Options passed after the path become **conditions**, and a route whose condition fails is skipped as if its pattern had not matched: - `provides: 'json'` matches only when the `Accept` header allows that type, and sets the response content type; - `host_name: /^admin\./` matches on the request's host; - `agent: /curl/` matches on the `User-Agent` header; - a custom one is defined with `set(:probability) { |value| condition { rand <= value } }` and used as `get '/', probability: 0.1`. Conditions are how two routes can share one path and still answer different clients. ## Where each parameter lands | Source | Example | Where you read it | |---|---|---| | Named segment | `'/payments/:id'` | `params['id']` or a block parameter | | Optional segment | `'/payments/:format?'` | `params['format']`, `nil` when absent | | Splat | `'/receipts/*.*'` | `params['splat']`, an Array | | Regexp route | `%r{/events/(\d+)}` | `params['captures']`, an Array | | Query string | `?mode=test` | `params['mode']` | | Form body | `application/x-www-form-urlencoded` | `params['field']` | The mechanics under that table: - `params` is a **`Sinatra::IndifferentHash`**: symbol keys are stored as strings, so `params[:id]` and `params['id']` return the same value. - Query and form fields are merged in first; path parameters are merged on top, so a path value **overrides** a query field with the same name unless the path value is `nil` (an absent optional segment). - Every value is a **String**, or an Array or nested Hash of strings for bracketed field names. Convert explicitly: `Integer(params['id'], 10)` raises `ArgumentError` on junk, where `to_i` would quietly return `0`. - A JSON request body is **not** parsed into `params`. Read `request.body` and parse it yourself. ## Block parameters and return values Captured values are also passed positionally to the block: `get '/receipts/*.*' do |path, ext|` receives the two splat pieces. What the block returns decides the response: - a **String** becomes the body; - an **Integer** becomes the status, for example `204`; - `[status, body]` or `[status, headers, body]` sets those parts; - any object that responds to `each` and yields strings becomes the body. ```ruby require 'sinatra' get '/receipts/*.*' do |path, ext| "#{path} as #{ext}" # GET /receipts/2026/r1.pdf => "2026/r1 as pdf" end get %r{/events/(\d+)} do "event #{params['captures'].first}" end ``` ## Traps interviewers probe - Expecting routes to be ranked, when the order of definition decides. - Treating `params['id']` as an Integer and comparing it with `42`. - Forgetting that `/payments/` misses a `/payments` route under the default `strict_paths`. - Looking for a JSON body's fields in `params`. - Assuming `params[:id]` is `nil` because "the keys are strings"; the indifferent hash makes both spellings work. - Forgetting that a query field named like a path segment is shadowed by the path value.

  • With get '/payments/:id' defined before get '/payments/refunds', which route answers GET /payments/refunds, and how do you fix it?
    The `:id` route answers, with `params['id']` set to `'refunds'`, because Sinatra runs the first matching route in definition order. Define the literal route first, constrain the segment (a regexp route such as `%r{/payments/(\d+)}`), or call `pass` inside the `:id` route when the value is not numeric.
  • In get '/payments/:id', what does params['id'] hold for GET /payments/7?id=9?
    `'7'`. Sinatra merges query and form fields into `params` first and the route's own parameters on top, keeping the path value unless it is `nil`. The query value 9 is shadowed; read `request.GET['id']` if you really need it.
  • How do you make one route answer only clients that accept JSON?
    Add the `provides:` condition: `get '/payments/:id', provides: 'json' do`. It checks the request's `Accept` header against the listed types, sets the response content type to the preferred match, and makes the route non-matching when nothing fits, so a later route or a 404 answers instead.

saying these in an interview costs you the question

  • Sinatra ranks routes so the most specific pattern wins.
  • params['id'] from '/payments/:id' is already an Integer.
  • Query-string fields are not in params; you must read request.GET.
  • A '/payments' route also answers '/payments/' by default.
  • params[:id] is nil because Sinatra stores parameter keys as strings.
open as a page

In a Sinatra payment-webhook receiver, how would you use a before filter and a helper to reject requests whose signature fails to verify?

level: middleimportance: must knowfreq 57%

basics

~10 s

Define a helper that computes an HMAC of the raw body and calls halt 401 on a mismatch, then call it from before '/webhooks/*'. The halt skips the route, while after filters still run.

open as a page

In Sinatra, what does erb :receipt render, and how do the layout, locals and instance variables reach that template?

level: juniorimportance: should knowfreq 42%

basics

~10 s

erb :receipt renders views/receipt.erb, wrapped in views/layout.erb when that file exists. The template runs with the request's app instance as self, so it sees instance variables and helpers; the locals option adds local variables.

open as a page

In a Sinatra route block, what is the difference between halt, pass and redirect, and what response does each one produce?

level: middleimportance: should knowfreq 50%

basics

~20 s

halt stops the request at once and sends the status, headers and body you give it. pass abandons the current route so the next matching route answers, or a 404. redirect sets Location and a 302 or 303, then halts.

open as a page

In Sinatra 4, how does a classic top-level app differ from a Sinatra::Base subclass in its defaults and startup, and how is each one run?

level: seniorimportance: should knowfreq 45%

basics

~20 s

require 'sinatra' sends top-level DSL calls to Sinatra::Application and starts a server at exit when the file runs directly. A Sinatra::Base subclass is a plain Rack app with logging, method override and auto-start off, started by run! or a Rack server.

open as a page

For a small payment-webhook receiver, when would you choose Sinatra over Rails, and what would you then have to add yourself?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Choose Sinatra when the service is a handful of endpoints with no HTML UI or shared models: it gives routing, filters, helpers and templates on Rack. Persistence, background jobs, code layout and duplicate-event handling are yours to add.

open as a page