skip to content

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%

answer

  1. require 'sinatra' versus 'sinatra/base'
  2. Delegator sends top-level DSL to Application
  3. at_exit run! when the file is $0
  4. Base: run, logging, method_override off
  5. Sinatra 4 run! needs the rackup gem

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.

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
ruby
# 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
end

go deeper

for a junior

Recall that require 'sinatra' gives the top-level DSL and auto-start, while require 'sinatra/base' needs a subclass of Sinatra::Base.

for a middle

Explain the Delegator on main, the at_exit run! hook, the settings that differ between Application and Base, and how set and configure behave.

for a senior

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.

for a principal

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.