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?
answer
- require 'sinatra' versus 'sinatra/base'
- Delegator sends top-level DSL to Application
- at_exit run! when the file is $0
- Base: run, logging, method_override off
- Sinatra 4 run! needs the rackup gem
basics
~20 srequire '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.
solid answer
~40 s`require 'sinatra'` loads `sinatra/main`, which extends Ruby's top-level object with `Sinatra::Delegator`, so `get`, `set` or `before` written at the top level land on `Sinatra::Application`. That means one app per process, `-p`, `-o` and `-e` read from `ARGV`, and an `at_exit` hook that calls `run!` when the app file is the script being run. `require 'sinatra/base'` with `class WebhookApp < Sinatra::Base` touches nothing global: `run`, `logging` and `method_override` default to false, and several apps can share a process. Start it with `run! if app_file == $0`, or serve the class from a Rack server, since the class itself responds to `call`. `set`, `enable` and `configure :production` work the same in both styles. In Sinatra 4, `run!` needs the `rackup` gem, and since 4.1 `host_authorization` limits development to local host names.
code
ruby · 13 lines# webhook_app.rb
require 'sinatra/base'
class WebhookApp < Sinatra::Base
configure :production do
enable :logging
set :host_authorization, { permitted_hosts: ['hooks.example.com'] }
end
post('/webhooks/payments') { 204 }
run! if app_file == $0 # start only when executed directly
endgo deeper
Recall that require 'sinatra' gives the top-level DSL and auto-start, while require 'sinatra/base' needs a subclass of Sinatra::Base.
Explain the Delegator on main, the at_exit run! hook, the settings that differ between Application and Base, and how set and configure behave.
Show how to run and mount modular apps as Rack endpoints, and handle Sinatra 4 changes: the rackup gem for run! and host_authorization per environment.
Weigh one process with several modular apps against separate services, considering deployment, isolation and the team that owns each.
## Two ways to load Sinatra **Classic style** is `require 'sinatra'`. That file, `sinatra/main`, does three things: - it extends Ruby's top-level `main` object with `Sinatra::Delegator`, which forwards `get`, `post`, `before`, `helpers`, `set`, `configure` and the rest to the class `Sinatra::Application`; - it parses command-line flags from `ARGV` when the app file is the script being run: `-p` port, `-o` bind address, `-e` environment, `-s` server, `-x` lock, `-q` quiet; - it registers an `at_exit` hook that calls `Application.run!` when the `run` setting is true, which it is when the file that required Sinatra is `$0`. So `ruby app.rb` starts a server once the file has loaded, and every top-level route in the process belongs to the one `Sinatra::Application`. **Modular style** is `require 'sinatra/base'` plus a subclass: `class WebhookApp < Sinatra::Base`. Nothing is added to `main`, nothing is run at exit, and each subclass is a separate application with its own routes, filters and settings. Several of them can live in one process, and a subclass of your own subclass inherits its routes and settings. ## Defaults that differ | Setting | Classic (`Sinatra::Application`) | Modular (`Sinatra::Base`) | |---|---|---| | `run` | true when the app file is `$0` | `false` | | `logging` | on outside the test environment | `false` | | `method_override` | `true` | `false` | | `app_file` | the file that required `sinatra` | the file that defined the subclass | | DSL on `main` | yes, via `Sinatra::Delegator` | no | Many other defaults are shared, including `protection` on, `sessions` off, `port` from `ENV['PORT']` or 4567 and `bind` of `localhost` in development and `0.0.0.0` elsewhere. There is nothing wrong with classic style for a one-file service; switch when you need more than one app per process or want to ship the app inside a library. ## Settings: set, enable, configure - `set :name, value` defines singleton methods `name`, `name=` and `name?` on the class; subclasses inherit them. A Proc value is re-evaluated on every read, which is how `bind` depends on the environment. - `enable :sessions` and `disable :logging` are `set` with `true` or `false`. - `configure :production do ... end` runs its block **once, while the class body loads**, and only when the environment matches; `configure` with no argument always runs. The environment comes from `APP_ENV`, then `RACK_ENV`, then defaults to `:development`. - Inside a request, `settings` returns the class, so `settings.port` reads a setting. ## Starting and mounting `run!` starts a built-in server: 1. It checks for `Rackup::Handler`. Since Sinatra 4.0 that lives in the separate **rackup** gem; without it Sinatra prints a warning suggesting `bundle add rackup puma` and exits with status 1. 2. It picks the first available handler from the `server` setting, which lists `puma` first on CRuby. 3. It binds `bind` and `port`, traps `INT` and `TERM` to stop cleanly, and calls any `on_start` block. A modular file usually ends with `run! if app_file == $0`, so it starts when executed and stays quiet when required by a Rack server or a test. The class also **is a Rack application**: `WebhookApp.call(env)` builds a prototype instance wrapped in its middleware once, then serves each request on a fresh copy of it. A Rack server can therefore run the class directly, and a Sinatra app placed in front of another Rack app acts as middleware, forwarding requests none of its routes match. `Sinatra.new { get('/') { 'hi' } }` builds an anonymous modular app the same way. ## Choosing between them - **Classic** fits a single file that is only ever run directly, such as a quick internal tool. - **Modular** fits anything packaged as a gem, tested by handing the class to a test harness, run behind a Rack server, or split into several apps. - Mixing is allowed: a classic file can require modular apps, and converting is mostly changing the `require` and wrapping the routes in a class. ## Version notes for Sinatra 4 - **4.0** supports Rack 3 only. Rack 3 moved its server handlers into the separate `rackup` gem, so `run!` now needs that gem. - **4.1** added the `host_authorization` setting. In development it permits `localhost`, `.localhost`, `.test` and any IP address, so a request arriving through a public tunnel host name gets 403 `Host not permitted`. In other environments the default is empty, which permits every host, so production services should list their host names in `permitted_hosts`.
- A payment provider's test webhook, sent through a public tunnel to your development laptop, gets 403 Host not permitted. Why?Since Sinatra 4.1 the `host_authorization` setting installs `Rack::Protection::HostAuthorization`. In development it permits only `localhost`, `.localhost`, `.test` and IP addresses, so the tunnel's host name is refused. Add it with `set :host_authorization, { permitted_hosts: [...] }`, keeping the local names you still need.
- What exactly does set :retry_limit, 3 create, and when is a Proc value evaluated?It defines singleton methods `retry_limit`, `retry_limit=` and `retry_limit?` on the class, inherited by subclasses and read as `settings.retry_limit`. A Proc value becomes the getter itself, so it is re-evaluated on every read rather than once.
- Why would you pick modular style even for a small service?It leaves `main` untouched, allows several apps in one process, makes the class easy to hand to a test or a Rack server without side effects, and does nothing at exit. Classic style is fine for a single-file app that is only ever run directly.
saying these in an interview costs you the question
- require 'sinatra/base' is the same as require 'sinatra' with fewer files.
- A Sinatra::Base subclass starts its own server when the file is executed.
- configure :production blocks are re-run on every request.
- Sinatra 4's run! works with only the sinatra gem installed.
- A classic top-level app cannot be served from a config.ru.