Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Middleware in Roda

Roda is a Rack application. Everything we said about Rack middleware in Part II applies directly. But Roda also has opinions about how middleware should be organized, and it introduces a couple of patterns that are worth understanding.

Using Rack Middleware with Roda

Standard Rack middleware works identically with Roda as with any other Rack application:

class App < Roda
  use Rack::Deflater          # gzip compression
  use Rack::Session::Cookie,  # sessions (Rack's version, not Roda's)
    secret: ENV['SESSION_SECRET']

  route do |r|
    r.get 'hello' do
      'Hello, World!'
    end
  end
end

use is inherited from Rack::Builder and adds middleware to the stack that wraps your Roda application. These middlewares run before any Roda routing.

Middleware vs. Plugins

Roda has two mechanisms for adding cross-cutting behavior: Rack middleware and Roda plugins. The choice matters.

Use Rack middleware for:

  • Behavior that applies to all Rack applications uniformly (compression, request logging)
  • Third-party middleware that doesn't know about Roda
  • Middleware that needs to run before Roda even sees the request

Use Roda plugins for:

  • Behavior that needs access to Roda's routing context
  • Features that need to know about sessions, current user, or other application state
  • Authentication and authorization that should happen within routes

The practical difference: middleware runs before Roda creates a request context. Plugins run inside that context. If your authentication middleware needs to set a current_user that your routes can access, you need a plugin, because middleware has no access to the Roda application instance.

The middleware Plugin

Roda includes a middleware plugin that lets you use a Roda app as middleware inside another Roda app:

# api_app.rb
class ApiApp < Roda
  plugin :json
  plugin :middleware  # makes this usable as middleware

  route do |r|
    r.on 'api' do
      r.get 'status' do
        {'status' => 'ok', 'app' => 'api'}
      end
    end
  end
end

# web_app.rb
class WebApp < Roda
  use ApiApp  # ApiApp handles /api/* requests, passes others through

  route do |r|
    r.get '/' do
      'Main web app'
    end
  end
end

When ApiApp receives a request that doesn't match any of its routes, it calls the next application in the stack (the @app in standard middleware terms). This is what the middleware plugin adds — the pass-through behavior.

Without plugin :middleware, a Roda app that doesn't match a route returns 404. With it, the app passes the request to the next layer.

Building an Auth Middleware in Roda Style

Here's a common pattern: an authentication middleware that sets the current user in the env, which is then read by the application's plugin:

# middleware/authenticate.rb
class Authenticate
  def initialize(app, header: 'HTTP_AUTHORIZATION')
    @app    = app
    @header = header
  end

  def call(env)
    token = env[@header]&.sub('Bearer ', '')

    if token
      user = User.find_by_token(token)
      env['current_user'] = user if user
    end

    @app.call(env)
  end
end
# plugins/current_user.rb
module Roda::RodaPlugins::CurrentUser
  module InstanceMethods
    def current_user
      # Read from env, set by the middleware
      env['current_user']
    end

    def require_authenticated!
      r.halt(401, {'error' => 'Authentication required'}) unless current_user
    end

    def require_admin!
      require_authenticated!
      r.halt(403, {'error' => 'Forbidden'}) unless current_user.admin?
    end
  end
end
# app.rb
class App < Roda
  use Authenticate  # runs before Roda, populates env['current_user']

  plugin :json
  plugin :halt
  plugin :current_user  # reads env['current_user'] in route context

  route do |r|
    r.get 'public' do
      'No auth required'
    end

    r.on 'private' do
      require_authenticated!  # from plugin

      r.get 'data' do
        {'user' => current_user.name, 'data' => [1, 2, 3]}
      end
    end

    r.on 'admin' do
      require_admin!  # from plugin

      r.get 'users' do
        User.all
      end
    end
  end
end

The middleware handles authentication (token verification, user lookup). The plugin provides ergonomic access to the result within Roda's context. Clean separation of concerns.

Conditional Middleware

Because Roda is a class and use is a class method, you can conditionally load middleware:

class App < Roda
  use Rack::Deflater                    # always compress
  use RequestLogger if $stdout.tty?    # log to console in development
  use Sentry::Rack::CaptureExceptions  # error tracking in production

  route do |r|
    # ...
  end
end

Or based on environment:

class App < Roda
  if ENV['RACK_ENV'] == 'development'
    use Rack::Lint   # validates Rack compliance
    use BetterErrors::Middleware, allow_ip: '127.0.0.1'
  end

  route do |r|
    # ...
  end
end

Middleware Ordering with Roda

The middleware ordering rules from Part II still apply, but Roda adds one consideration: plugins that modify request or response handling (like sessions or csrf) run inside the Roda instance, after all Rack middleware. Rack-level session middleware runs before Roda; Roda's session plugin runs inside Roda.

This means you shouldn't mix Rack session middleware with Roda's session plugin:

# Don't do this
class App < Roda
  use Rack::Session::Cookie, secret: 'secret1'  # Rack-level sessions
  plugin :sessions, secret: 'secret2'           # Roda-level sessions
  # They'll conflict — two different session stores
end

# Do this instead
class App < Roda
  plugin :sessions, secret: ENV['SESSION_SECRET']  # Roda handles sessions
end

Roda's session plugin uses a slightly different cookie format than Rack::Session::Cookie, so if you're migrating from a Rack session setup, you'll need to handle the transition carefully.

Inspecting the Middleware Stack

To see what middleware is in your stack:

# In a Rack app or Rails console
App.middleware.each { |m| p m }

Or at the Rack level:

# config.ru
require_relative 'app'

# Introspect before running
puts "Middleware stack:"
app = App
while app.respond_to?(:app)
  puts "  #{app.class}"
  app = app.app
end

run App

In production, it's worth auditing your middleware stack. Every middleware is code that runs for every request. If you have middleware you're not using, remove it.

The Right Mental Model

Think of a Roda application as three nested layers:

[Rack middleware layer]
  Rack::Deflater
  YourAuthMiddleware
  Rack::RequestId
    ↓ env hash passes through here
[Roda routing layer]
  plugin :sessions reads/writes cookies
  plugin :csrf validates tokens
  r.on / r.is / r.get match paths and methods
    ↓ reaches your route block
[Your application layer]
  current_user
  business logic
  data access

Middleware is for infrastructure concerns that don't need to know they're running in Roda. Plugins are for application concerns that benefit from Roda's routing context.

When you're unsure which to use: if it needs access to session, current_user, or any Roda-specific context, it's a plugin. If it treats the request as an opaque Rack env hash, it's middleware.

Next: testing Roda applications, which is one of the genuine pleasures of this stack.