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?
answer
- one list per verb, definition order
- first match runs, the rest never do
- params takes string or symbol keys
- splat array, captures for regexps
- strict_paths is true by default
basics
~20 sSinatra 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 sEach `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 linesrequire '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}"
endgo deeper
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.
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.
Show how route order, strict_paths and string-typed params cause real bugs, and how conditions, pass or narrower patterns fix overlapping routes.
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.