Goal: To build a highly simplified implementation of the controller pattern to better understand Rails controllers.
In its essence, a controller class in Rails is a coordinator. It accepts the incoming HTTP request and builds the data required by the view using the domain models.
If you could take only one lesson from this post, it's this:
The incoming HTTP request doesn't hit your Rails controller's action method automagically out of thin air (something I used to think a while ago when I wrote ASP.NET MVC controllers), but there's a bunch of framework code behind the scenes that receives the request from the app server like Puma, processes it, creates an instance of the controller (just like any other class), and calls the action method on it. Then it takes the response returned by the action, processes it, and sends it back to the app server, which returns it to the browser.
I hope that you'll have a much better understanding and appreciation for Rails controllers after finishing this lesson.
What we've so far:
To recap, this is where we were at the end of the previous post.
- The
config/routes.rb
file creates the routes. The blocks return the HTML response corresponding to each path.
# config/routes.rb
require_relative '../router'
Router.draw do
get('/') { "Akshay's Blog" }
get('/articles') { 'All Articles' }
get('/articles/1') do |env|
puts "Path: #{env['REQUEST_PATH']}"
"First Article"
end
end
- The router stores the path-to-handler route mapping and uses the handler to generate the response.
# router.rb
require 'singleton'
class Router
include Singleton # 1
attr_reader :routes
class << self
def draw(&blk) # 2
Router.instance.instance_exec(&blk) # 3
end
end
def initialize
@routes = {}
end
def get(path, &blk)
@routes[path] = blk
end
def build_response(env)
path = env['REQUEST_PATH']
handler = @routes[path] || ->(env) { "no route found for #{path}" }
handler.call(env) # pass the env hash to route handler
end
end
- The application generates the response using the router and sends it to the application server.
# app.rb
require_relative './config/routes'
class App
def call(env)
headers = { 'Content-Type' => 'text/html' }
response_html = router.build_response(env)
[200, headers, [response_html]]
end
private
def router
Router.instance
end
end
It works as expected; however, there's a small issue with the above structure.
In the routes file, we're defining all the request-handling logic as blocks. For simple routes or for debugging purposes, it's totally fine. However, for regular routes that involve a bit more logic, you may want to organize them using the "controller" classes.
The 'Controller' pattern lets you group all the request-handling logic for a route into a single class.
For example, an ArticlesController
class might handle all incoming requests related to articles, such as creating, displaying, updating, and deleting articles, a UsersController
class will handle all user-specific requests, and so on.
Not only will it keep our code clean and tidy, but it will also limit the complexity as we add new functionality to our application, resulting in maintainable code.
Let's examine one way to implement the 'Controller' pattern.