ruby 91 lines · 3 tabs

Mounting Modular Sinatra Apps Under Route Prefixes with Rack::URLMap

Shared by codesnips Jul 2026
3 tabs
require 'sinatra/base'
require 'json'

class HealthApp < Sinatra::Base
  configure do
    set :dump_errors, true
    disable :show_exceptions
    set :default_content_type, 'application/json'
  end

  get '/' do
    content_type :json
    JSON.generate(status: 'ok', service: 'health', uptime: Process.clock_gettime(Process::CLOCK_MONOTONIC).round)
  end

  get '/ready' do
    content_type :json
    ready = defined?(DB) ? DB.connected? : true
    status(ready ? 200 : 503)
    JSON.generate(ready: ready)
  end
end
3 files · ruby Explain with highlit

This snippet demonstrates the modular style of Sinatra, where each app subclasses Sinatra::Base instead of using the classic top-level DSL, and several such apps are composed into a single Rack application with Rack::URLMap. The pattern is useful when a codebase grows past a single monolithic app.rb: cohesive concerns (a health probe, a JSON API, an admin surface) each become their own self-contained Sinatra class, and mounting decisions live in config.ru rather than being tangled into route definitions.

In api/health_app.rb, HealthApp is a minimal modular app that only knows about / and /ready. Because it is mounted under a prefix later, its routes are written relative to that mount point — the app itself is prefix-agnostic. The configure block sets :dump_errors and disables :show_exceptions so the probe returns clean JSON in production rather than Sinatra's HTML error page.

In api/notes_app.rb, NotesApp is a more realistic resource. It uses a before filter to force Content-Type: application/json, a helpers block for a shared json_body reader that parses the request body once, and halt to short-circuit with proper status codes. Note that inside the app it still references /notes and /notes/:id; the mount prefix is invisible here, which keeps the app independently testable with Rack::Test against its own root.

The glue is config.ru. Rack::URLMap (constructed via the map DSL that rackup provides) matches on the longest path prefix, so /api/notes is routed to NotesApp and everything else under /health reaches HealthApp. A subtle detail is that Rack strips the matched prefix from PATH_INFO and moves it into SCRIPT_NAME before handing the request to the mounted app, which is exactly why the inner routes stay prefix-free. Shared middleware such as Rack::Deflater and a request-id logger is applied once at the top, wrapping every mounted app uniformly.

The main trade-off is that URL generation across mounts requires care, since each app only knows its own SCRIPT_NAME; using request.script_name when building absolute links keeps URLs correct regardless of where an app is mounted. This composition approach is a lightweight alternative to reaching for a heavier framework when a small service needs a few clearly separated surfaces.


Related snips

Share this code

Here's the card — post it anywhere.

Mounting Modular Sinatra Apps Under Route Prefixes with Rack::URLMap — share card
Link copied