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.

# 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
# 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
# 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.