ruby / web framework
I use a ~500-line Ruby and Rack-based web framework. The app is a monolith serving HTML with templates, Postgres via DB, middleware, and background work via job queues.
Why Rack, not Rails
Rails carries opinions on everything from asset pipelines to mailers. Most are irrelevant to any given application, yet they arrive anyway — as dependencies to manage, abstractions to navigate, and conventions for an AI to misremember.
A Rack app contains only what it uses. The entire framework fits in a few files, which makes it simple to read, quick to change, and easy to test. When something breaks, the stack trace is short and points to your code rather than framework internals.
The small surface area also helps AI-assisted development. A language model can hold the whole framework in context rather than drawing on a decade of shifting Rails conventions. Code generation becomes faster and more accurate.
If the application outgrows Ruby, a Rack app this size can be ported (to Go, say) one handler at a time.
Rack app with routing
The core is a Rack app that maps URL patterns to handler classes. Handler classes and their constructor args are stored at boot; a fresh instance is created per-request for thread safety:
module Framework
class App
def initialize
@routes = {"GET" => [], "POST" => []}
end
def get(path, klass, *args, **kwargs)
@routes["GET"] << [compile(path), path, klass, args, kwargs]
end
def post(path, klass, *args, **kwargs)
@routes["POST"] << [compile(path), path, klass, args, kwargs]
end
def redirect(from, to)
@routes["GET"] << [compile(from), from, :redirect, [to], {}]
end
# Rack interface
def call(env)
req = Rack::Request.new(env)
result = match(req.request_method, req.path_info)
if result.nil?
return [404, {"Content-Type" => "text/html"}, [File.read("public/404.html")]]
end
klass, args, kwargs, params = result
if klass == :redirect
return [301, {"Location" => args[0]}, []]
end
env["router.params"] = params
response = catch(:halt) do
klass.new(*args, **kwargs).call(env)
end
response
end
private def match(method, path)
@routes[method]&.each do |pattern, _, klass, args, kwargs|
if (m = pattern.match(path))
return [klass, args, kwargs, m.named_captures]
end
end
nil
end
private def compile(path)
pattern = path.gsub(/:([a-z_]+)/, '(?<\1>[^/]+)')
Regexp.new("\\A#{pattern}\\z")
end
end
end
Routes are registered at boot:
app = Framework::App.new
app.get "/health", HealthHandler::Check, db
app.get "/companies/:id", CompaniesHandler::Show, db
app.post "/companies/create", CompaniesHandler::Create, db
app.redirect "/old-path", "/new-path"
run app
Path parameters like :id become named captures
available via env["router.params"].
The catch(:halt) in call allows handlers
to short-circuit with throw :halt, response.
This is useful for auth checks that need to redirect
without awkward return if patterns.
Handlers
Handler classes are registered at boot with their dependencies.
The router creates a fresh instance per-request,
so there is no shared mutable state across threads.
Subclasses implement handle (not call)
so setup and auth are automatic:
module Framework
class Handler
attr_reader :db, :req, :params, :session
def initialize(db)
@db = db
end
def call(env)
setup(env)
require_login
resp = handle
resp = wrap_html(resp)
resp
end
def setup(env)
@env = env
@req = Rack::Request.new(env)
@params = @req.params.merge(env["router.params"] || {})
@session = env["rack.session"] || {}
end
def handle
raise NotImplementedError, "#{self.class} must implement #handle"
end
# Returns an HTML string. For partials and composition.
def render(template, **locals)
all_locals = locals.merge(default_locals)
Template.render(template, all_locals, layout: nil)
end
# Returns a Rack response with layout. For full page renders.
def page(template, title: "EDS", **locals)
all_locals = locals.merge(default_locals).merge(title: title)
html = Template.render(template, all_locals, layout: "layouts/application")
[200, {"Content-Type" => "text/html"}, [html]]
end
def redirect(location)
[303, {"Location" => location}, []]
end
def head(status, headers = {})
[status, headers, []]
end
private def require_login
if current_user
return true
end
@session[:return_to] = @req.fullpath
throw :halt, redirect("/login")
end
# Auto-wrap string returns from handle into Rack responses.
private def wrap_html(resp)
if resp.is_a?(String)
[200, {"Content-Type" => "text/html"}, [resp]]
else
resp
end
end
private def default_locals
{
current_user: current_user,
params: params,
session: @session,
flash: @env["app.flash"] || {}
}
end
end
end
render returns an HTML string, useful for composing partials
or returning ajax fragments.
page wraps the template in a layout and returns a Rack response.
call auto-wraps string returns from handle via wrap_html,
so ajax handlers can return render(...) directly.
A handler hierarchy keeps auth secure by default.
Handler requires login.
PublicHandler skips auth (for login pages, webhooks, health checks).
AdminHandler adds require_admin.
A health check handler:
module HealthHandler
class Check < Framework::PublicHandler
def handle
db.exec("SELECT 1")
head 200
end
end
end
A typical page handler:
module CompaniesHandler
class Show < Framework::Handler
PageData = Data.define(:name, :status)
def handle
co = Companies::Find.new(db).call(id: params["id"])
if co.nil?
return [404, {"Content-Type" => "text/html"}, [File.read("public/404.html")]]
end
data = PageData.new(
name: co["name"],
status: co["status"]
)
page "companies/show", title: data.name, data: data
end
end
end
Encrypted cookies
For sensitive values like auth tokens, use AES-256-GCM encryption rather than just signing:
class EncryptedCookieJar
def initialize(request_cookies, secret)
@request_cookies = request_cookies
@key = OpenSSL::Digest::SHA256.digest(secret)[0, 32]
end
def [](name)
data = @request_cookies[name]
if data.nil?
return nil
end
decrypt(data)
end
private def encrypt(plaintext)
cipher = OpenSSL::Cipher.new("aes-256-gcm").encrypt
cipher.key = @key
iv = cipher.random_iv
ciphertext = cipher.update(plaintext) + cipher.final
tag = cipher.auth_tag
Base64.urlsafe_encode64(iv + tag + ciphertext)
end
private def decrypt(data)
raw = Base64.urlsafe_decode64(data)
cipher = OpenSSL::Cipher.new("aes-256-gcm").decrypt
cipher.key = @key
cipher.iv = raw[0, 12]
cipher.auth_tag = raw[12, 16]
cipher.update(raw[28..]) + cipher.final
rescue
nil
end
end
Use Rack::Session::Cookie (signed) for the session.
Use the encrypted cookie jar only for the auth token.
Boot sequence
Require gems explicitly rather than using Bundler.require.
Explicit requires make the dependency graph visible.
Bundler.require hides what you depend on
and adds autoload magic that slows boot.
Running with Puma
require "puma"
require_relative "lib/db"
require_relative "lib/framework/boot"
require_relative "lib/framework/server"
DB.configure do |c|
c.pool_size = ENV.fetch("WEB_CONCURRENCY").to_i * ENV.fetch("WEB_THREADS").to_i
c.reap = true
end
conf = Puma::Configuration.new do |c|
c.app Framework::Server.app(DB.pool)
c.environment ENV.fetch("APP_ENV")
c.bind "tcp://0.0.0.0:#{ENV.fetch("PORT")}"
c.preload_app!
c.threads ENV.fetch("WEB_THREADS").to_i, ENV.fetch("WEB_THREADS").to_i
c.workers ENV.fetch("WEB_CONCURRENCY").to_i
c.before_worker_boot do
DB.reset_pool! # fresh connections after fork
end
end
Puma::Launcher.new(conf).run
Testing handlers
Handler tests use Rack::Test against the routes
without the full middleware stack.
See ruby / test for the test framework.
class HandlerTest < Test
include Rack::Test::Methods
def app
Framework::Server.routes(db)
end
def sign_in
set_cookie("remember_token=test")
end
end
class HealthTest < HandlerTest
def test_health
resp = get("/health")
ok { resp.status == 200 }
end
end
class CompaniesShowTest < HandlerTest
def test_show
sign_in
co = insert_company(name: "Acme Inc")
resp = get("/companies/#{co.id}")
ok { resp.status == 200 }
ok { resp.body.include?("Acme Inc") }
end
end
Response modes
Every handler declares which response modes it supports:
# Ajax-only (forms, partials)
class CompaniesHandler::Edit < Framework::Handler
modes :ajax
def handle
render "companies/edit", data: data
end
end
# Dual-mode (full page + ajax partial)
class CompaniesHandler::Show < Framework::Handler
modes :html, :ajax
def handle
if ajax?
render "companies/ajax_show", data: data
else
page "companies/show", title: data.name, data: data
end
end
end
# HTML-only (default; no modes declaration needed)
class DashHandler::Home < Framework::Handler
def handle
page "dash/home", title: "Home", data: data
end
end
The base class enforces this. Requests for unsupported modes return 400.
See ruby / test for ping test coverage that guarantees every handler mode has a test.
HTTP methods
GET and POST only. HTML forms can only submit GET and POST. Two verbs simplify routing, middleware, and CSRF handling.