Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
28: Rack Middleware Concepts
I've spent a lot of time in Rails, but for a long while, I treated the stuff happening "under the hood" as magic. Specifically, the way a request actually hits your code. In Ruby, that magic is almost always Rack. If you've ever looked at a config.ru file and wondered why it's there, you're looking at the entry point for Rack.
Instead of just reading the documentation, let's actually build something. I want to add a custom header to every single response in my application—something like X-Processed-By: MyCustomEngine. I could do this inside my controller, but that's tedious. I want it to happen automatically, regardless of which route is hit.
The simplest possible Rack app
First, I need a baseline. A Rack application is just an object that responds to call, takes an env hash, and returns an array with three specific elements: the status code, the headers, and the body. Let's start there.
# app.rb
class SimpleApp
def call(env)
[200, { "Content-Type" => "text/plain" }, ["Hello from the app!"]]
end
end
# To run this, we'd use a config.ru file:
# run SimpleApp.new
If I run this with rackup, it works perfectly. But now I'm stuck. I want that custom header. If I add it inside SimpleApp, I'm just hardcoding it into the application logic. What if I have ten different apps? I don't want to rewrite that header logic ten times. I need a way to "wrap" the app.
Wrapping the application
I'll try creating a class that takes the app as an argument. I'll call it HeaderInjector. The idea is that the injector handles the request first, then passes it along to the actual app.
class HeaderInjector
def initialize(app)
@app = app
end
def call(env)
# I'll just call the app and return the result
@app.call(env)
end
end
Right now, this does absolutely nothing. It's just a passthrough. But wait—the @app.call(env) line returns that three-element array (status, headers, body). Since I have that array in my hands before it goes back to the web server, I can actually mutate it.
Interacting with the response
Let's try to grab that headers hash and shove our custom key into it. This is where the "middleware" concept actually clicks: the middleware sits between the server and the app.
class HeaderInjector
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
# Here is the magic: we modify the headers hash before it leaves
headers["X-Processed-By"] = "MyCustomEngine"
[status, headers, body]
end
end
I'll update my config.ru to use this:
# config.ru
require_relative 'app'
use HeaderInjector
run SimpleApp.new
When I run this and check the network tab in my browser (or use curl -I), there it is: X-Processed-By: MyCustomEngine. I didn't touch a single line of code in SimpleApp, yet the response changed. That's the power of middleware.
Thinking about the "Pipeline"
I started wondering: what if I want to do something before the app even sees the request? For example, maybe I want to log the request path. I can just add a line before the @app.call(env).
def call(env)
puts "Incoming request to: #{env['PATH_INFO']}"
status, headers, body = @app.call(env)
headers["X-Processed-By"] = "MyCustomEngine"
[status, headers, body]
end
Now I have a pipeline. The request flows down through the middleware (where I log the path), hits the app, and then the response flows back up through the middleware (where I inject the header) before finally hitting the user. If I add five different use statements in my config.ru, I've essentially built an onion. Each layer of middleware can inspect the request on the way in and tweak the response on the way out.
It's a remarkably simple pattern, but it's how almost every major Ruby framework handles things like sessions, cookies, and authentication. They aren't "features" of the framework's core logic; they're just layers of Rack middleware.
📋 Practical Task
Build a Request-Duration Logger Middleware
Your task is to create a Rack middleware called ResponseTimer. This middleware should calculate how long it takes for the application to process a request and then add that duration (in milliseconds) to a custom response header called X-Runtime-Ms.
- Create a class
ResponseTimerthat follows the Rack middleware pattern (initializeandcall). - Capture the start time before calling
@app.call(env). - Capture the end time after the app returns the response.
- Calculate the difference, convert it to milliseconds, and add it to the
headershash. - Ensure the middleware returns the standard Rack array:
[status, headers, body].
Test your work: Use a simple Rack app and a config.ru file with use ResponseTimer to verify that the X-Runtime-Ms header appears in your HTTP response.
There are no comments for now.