Rails Companion Learn to Build a Web Application in Ruby, without Rails Akshay Khot

  • Move Introduction
    Open Introduction

    Rails is great for building web apps. But it can be quite overwhelming if you don't know how web applications work.

    In this course, we'll build a simple but complete app in plain Ruby without Rails, to get a deeper understanding and appreciation of everything Rails does for us. Finally, we will deploy it to a web server, which will give you a good understanding of the deployment process for Ruby-based web applications, including Rails.

    We will start with a very simple Ruby script, but progress quickly to add various features like routing, controllers, models, and views.

    At the end of the course, you will have a fully-functioning web application written in Ruby. Finishing this course will give you a great foundation to learn Rails.

    Introduction 124 words
  • Move Let's Build
    Open Let's Build

    Project Setup

    Let's Build
  • Move Setup Project Directory
    Open Setup Project Directory

    In this lesson, we will setup the project directory for our application along with Ruby's package manager tool called Bundler.

    Note: I assume you have already installed Ruby on your machine. If not, download it using rbenv tool, which lets you manage multiple versions of Ruby.

    Let's create a brand-new directory named weby (yes, it stands for web + ruby) and switch into it.

    $ mkdir weby
    
    $ cd weby
    

    Next, we'll install bundler, a package (gem) manager for Ruby.

    What's a gem?

    A gem is a packaged Ruby application (similar to packages in JavaScript or PHP) that provides a specific functionality, such as logging, file upload, authentication, etc. When you build software, you install gems written by others instead of writing everything yourself.

    What's bundler?

    Bundler provides a consistent environment for Ruby projects by tracking and installing the exact gems that the application depends on. Its goal is to make sure that Ruby applications run the same code on every

    Setup Project Directory 305 words
  • Move Install Puma App Server
    Open Install Puma App Server

    To run our web application, we need an application server.

    The purpose of an application server is to take an incoming HTTP request from the web server and pass it to our application. Our application processes the request, builds the response HTML, and hands it back to the application server.

    request-response-flow.jpeg

    An application server is different from a web server such as Apache, Nginx, or Caddy which accept incoming requests from the Internet and pass them to the application server, which talks to the Rails application.

    Typically you start with one web server and one application server, all on a single server instance.

    app-servers.jpeg

    As your app grows and starts receiving more traffic, you may want to use multiple Rails applications to handle the incoming requests.

    Install Puma App Server 302 words
  • Move Install the Rack Gem
    Open Install the Rack Gem

    GOAL: To install the Rack gem so Puma can talk to our web application.

    All Ruby-based application servers (like Puma) and web applications follow the Rack specification.

    What is Rack?

    Rack provides a simple interface that allows Ruby-based web frameworks and application servers communicate with each other. The Rack specification has two conditions:

    1. The application should have a call method that takes an env object representing the incoming HTTP request.
    2. The application should return an array containing the status, headers, and response.

    So that is Rack, a specification. However, in addition to being an abstract protocol, Rack is also a gem. It provides a bunch of useful tools, such as middleware and a convenient DSL (domain-specific language) to run rack-compliant web applications.

    Let's install the Rack gem using Bundler.

    $ bundle add rack
    

    If you want to learn about Rack in depth right now (both the specification as well as the gem), I suggest you read this

    Install the Rack Gem 191 words
  • Move Create a Web Application
    Open Create a Web Application

    GOAL: To create a Rack-compliant web application that can talk to Puma application server.

    Create the following Ruby script called app.rb in the weby directory. This is our application.

    Yes, IT IS our entire app.

    # weby/app.rb
    
    class App
      def call(env)
    
        # a hash containing response headers
        headers = {
          "Content-Type" => "text/html"
        }
    
        # an array containing the HTML response string
        response = ["<h1>Hello World!</h1>"]
    
        # an array containing
        # 1. HTTP status code
        # 2. HTTP headers 
        # 3. HTTP response
        [200, headers, response]
      end
    end
    

    The above script is a complete Rack-compliant web application. As you can see, it follows the Rack protocol.

    1. It has a call method that takes an env object.
    2. It returns an array containing the status, headers, and response.

    Now let's see how we can make the Puma app server talk to our application using the Rack protocol.

    Create a Web Application 139 words
  • Move Run the Application
    Open Run the Application

    GOAL: To launch our web application!

    To launch the web server, run the puma command from the web directory. Alternatively, you can also run rackup and it will work the same.

    The benefit of the rackup command is that it's app-server agnostic. So it will work even if you're using a different app-server like Thin instead of Puma.

    $ puma
    
    Puma starting in single mode...
    * Puma version: 6.4.0 (ruby 3.2.2-p53) ("The Eagle of Durango")
    *  Min threads: 0
    *  Max threads: 5
    *  Environment: development
    *          PID: 10910
    * Listening on http://0.0.0.0:9292
    Use Ctrl-C to stop
    

    Now our web server is running and serving our application. Open your browser and go to http://0.0.0.0:9292 URL.

    hello-world.png

    Congratulations! You have the simplest web application written in Ruby.

    To summarize what we've done so far,

    1. We installed an application server (Puma) that accepts HTTP request and forwards it
    Run the Application 284 words
  • Move Reload App when the Code Changes
    Open Reload App when the Code Changes

    GOAL: To use a Rack middleware to reload the application whenever we change the source code.

    At this point, we have a small problem.

    If you make a change in your application, it won't be reflected in the browser. For this, you need to first

    1. Stop the server by pressing ctrl + c on the keyboard.
    2. Make the change.
    3. Restart the server by running rackup or puma command.

    As you can probably guess, it can get tedious to restart the server after every change.

    Is there a better way?

    Yes! The Rack gem ships with a Rack::Reloader middleware that reloads the application after changing the source code.

    A middleware is a small, focused, and reusable application that provides useful functionality to your main app.

    Middleware sits between the user and the application code. When an HTTP request comes in, the middleware can intercept, examine, and modify it. Similarly, it can examine and modify the HTTP response

    Reload App when the Code Changes 250 words
  • Move What's Next?
    Open What's Next?

    Currently, our app is returning a hard-coded string response directly from the Ruby code, which is not ideal. We will learn how to separate the "view" from the application logic and move it to a different part of the codebase.

    However, before that, we need to understand a core piece of infrastructure in Rails: Rack, both the protocol and the gem.

    Let's move on to the next lesson, where we will dive deep into Rack to understand how it works.

    What's Next? 78 words
  • Move Understanding Rack: Protocol and Gem
    Open Understanding Rack: Protocol and Gem

    Understanding Rack: Protocol and Gem

    Understanding Rack: Protocol and Gem
  • Move Introduction
    Open Introduction

    You’ve been around in the Rails world for a while. You know your way around rails. But you keep hearing the word ‘Rack’ and don’t really understand what it is or what it does for you.

    You try to read the documentation on the Rack Github repository or the Rails on Rack guides, but the only thing it does is add to the confusion. Everyone keeps saying that it provides a minimal, modular, and adaptable interface for building web applications. But what the heck does that really mean?

    If there’s a list of topics that confuses most new Rails developers, Rack is definitely up there at the top. When I started learning Ruby and Rails last year, Rack took a really long time to wrap my head around. If you are in the same boat, fear not. In this article, I will try to explain pretty much everything that you need to know about Rack as a Ruby and Rails developer.

    What is Rack?

    Introduction 296 words
  • Move The Rack Protocol
    Open The Rack Protocol

    Here’s the standard definition of Rack that you may have heard.

    Rack provides a minimal, modular, and adaptable interface for developing web applications in Ruby.

    At the very basic level, Rack is a protocol, just like HTTP. Rack provides a layer between the framework (Rails) & the web server (Puma), allowing them to communicate with each other.

    The Rack protocol allows you to wrap HTTP requests and responses in simple interfaces. This simplifies the API for web servers and frameworks into a single method call.

    This is exactly similar to how your browser talks to the web server using the HTTP protocol. That allows any client (Chrome, Firefox, or even terminal commands) to talk to any backend server written in any language (PHP, .NET, Ruby, etc.) so you can access the website.

    When we say an application is Rack-compliant, it means the following:

    1. It has a call method that accepts a single argument env, containing all the data about the request, and

    2. It returns an array containing:

    The Rack Protocol 205 words
  • Move The Rack Gem
    Open The Rack Gem

    So far, we’ve only talked about the Rack specification. But there’s also a gem called Rack. You can find the source code for it on Github, read the documentation on Rubydoc, and install it like any other Ruby gem.

    gem install rack
    

    Why do we need the Rack gem if the rack-compliant web servers and frameworks can communicate without it?

    There are a few important reasons. Let's explore them one-by-one.

    The Rack Gem 69 words
  • Move Middleware Toolbox
    Open Middleware Toolbox

    Because the Rack interface is so simple, you can use any code that implements this interface in a Rack application.

    This allows you to build small, focused, and reusable applications which work together to provide different functionalities. These mini-components are known as Middleware.

    Middleware sits between the user and the application code. When an HTTP request comes in, the middleware can intercept, examine, and modify it. Similarly, it can examine and modify the HTTP response before forwarding it to the user.

    Middleware is very useful for writing logic that is not specific to your web application, such as authenticating the request, logging, or exception handling. It focuses on doing one thing and doing it well.

    middleware.png

    For example, here’s a middleware that logs to the console

    1. before it passes the request to the next application in the pipeline and

    2. after it receives the response on its way out.

    Middleware Toolbox 454 words
  • Move Tools to Build Rack Apps + Middleware
    Open Tools to Build Rack Apps + Middleware

    The Rack gem provides the infrastructure code that you can use to build your own web application, web framework, or middleware. It allows to quickly prototype or build stuff without doing the same repetitive tasks again and again.

    Here are some helpers you can use with Rack:

    • Rack::Request, which also provides query string parsing and multipart handling.

    • Rack::Response, for convenient generation of HTTP replies and cookie handling.

    • Rack::MockRequest and Rack::MockResponse for efficient and quick testing of Rack application without real HTTP round-trips.

    • Rack::Cascade, for trying additional Rack applications if an application returns a not found (404) or method not supported (405) response.

    • Rack::Directory, for serving files under a given directory, with directory indexes.

    • Rack::MediaType, for parsing content-type headers.

    • Rack::Mime, for determining content-type based on file extension.Since the Rack interface is so simple, anyone can build and publish

    Tools to Build Rack Apps + Middleware 166 words
  • Move The Rackup Command
    Open The Rackup Command

    The Rack gem ships with the rackup command (update: as of Rack 3, the rackup command has been moved to a separate rackup gem. See this pull-request and related discussion). It is a useful tool for running Rack applications with a web server. It uses the Rack::Builder DSL (domain-specific language) to configure middleware and easily compose applications.

    The rackup command automatically figures out the environment it is run in, and runs your application as WEBrick, Puma, or any web server—all from the same configuration.

    Let’s try that now.

    So far, we’ve been launching our web application using a web server-specific command, i.e. pumathin start, or unicorn. Using rackup lets us be web server agnostic. In the same directory containing your config.ru file, run rackup command.

    $ gem install rackup
    $ rackup
    
    Puma starting in single mode...
    * Puma version: 5.6.4 (ruby 2.7.1
    
    The Rackup Command 173 words
  • Move Convenient DSL
    Open Convenient DSL

    If you aren’t familiar with the term DSL, it stands for Domain Specific Language. It is a programming language with a higher level of abstraction optimized for a specific class of problems.

    A DSL uses the concepts and rules from the field or domain. For example, Ruby on Rails provides a DSL for building web applications.

    Rack provides such a DSL consisting of the following methods:

    • run: It takes a Ruby object responding to the call method as an argument and invokes the call method on it. We used this method in our example at the beginning of this post.

    • map: It takes a string and a block as parameters and maps incoming requests against the string. If it matches, Rack will run the block to handle that request. This is similar to how routing works in Rails.

    • use: It includes the middleware to the rack application.

    Additionally, the Rack::Builder class helps you with iteratively composing Rack applications

    Convenient DSL 253 words
  • Move What's Next?
    Open What's Next?

    To summarize what we’ve learned so far, the Rack protocol powers the web in the Ruby world. It consists of two things:

    1. The Rack specification (protocol) provides a simple interface that allows web servers and applications to talk to each other.

    2. The Rack gem provides middleware, tools, and DSL that let us iteratively compose our Rack-compliant applications.

    Additionally, Rack middleware components allow you to separate the application logic from peripheral concerns like authentication, logging, and error handling. You can use any middleware built by the community for everyday use cases.

    With that understanding, let's get back to our web application. In the next lesson, we will build a simple router in Ruby.

    What's Next? 114 words
  • Move Build Your Own Router in Ruby
    Open Build Your Own Router in Ruby

    Build Your Own Router in Ruby

    Build Your Own Router in Ruby
  • Move Introduction
    Open Introduction

    In this lesson, we'll learn how routing works in its essence and build our own router from scratch, without using any third-party gems. We'll use some metaprogramming, but I promise it's nothing complicated.

    Our router will look somewhat similar to Rails. It will accept a URI and a block, providing a very simple and expressive method to define routes and behavior without complicated routing configuration.

    Router.draw do
      get('/') { 'Hello world' }
    end
    

    Alright, let's start by understanding what a router is and what it does, in the context of a web application.

    Introduction 95 words
  • Move What's a Router?
    Open What's a Router?

    A router is that part of the web application that determines where the incoming request should go. It figures it out by examining the request URL and then invoking a pre-defined function (or a handler) for that path. Internally, it stores the mapping between the URL patterns and the corresponding handlers.

    router.png

    In Ruby on Rails, the router sends the incoming request to an action method on a controller. In the next lesson, we'll build our own controllers and modify our router (that we'll build today) to dispatch the request to the controller#action code.

    Let's see how we can build our simple router.

    What's a Router? 101 words
  • Move Current Setup
    Open Current Setup

    At this stage, you should have the following script, which is a barebone, simple-yet-complete web application.

    class App
      def call(env)
    
        # a hash containing response headers
        headers = {
          "Content-Type" => "text/html"
        }
    
        # an array containing the HTML response string
        response = ["<h1>Hello World</h1>"]
    
        # an array containing
        # 1. HTTP status code
        # 2. HTTP headers 
        # 3. HTTP response
        [200, headers, response]
      end
    end
    

    Right now, our application does only one thing. Whenever a request arrives, it returns the response Hello World to the browser. It sends the same response, regardless of the request URL.

    ➜ curl localhost:9292
    <h1>Hello World</h1>
    
    ➜ curl localhost:9292/posts
    <h1>Hello World</h1>
    
    ➜ curl localhost:9292/posts/new
    <h1>Hello World</h1>
    

    Having a web application that returns the same response for every request isn't very exciting... or useful! Let's make it smart by returning a different

    Current Setup 224 words
  • Move The Router Class
    Open The Router Class

    Let's create a new Router class in the current directory as follows. It maintains a @routes Hash as the internal data structure to store the URL patterns along with their corresponding handlers.

    A handler is simply a Ruby block that will be called when our app receives a request to the corresponding path.

    # weby/router.rb
    
    class Router
      def initialize
        @routes = {}
      end
    
      def get(path, &blk)
        @routes[path] = blk
      end
    
      def build_response(path)
        handler = @routes[path] || -> { "no route found for #{path}" } 
        handler.call 
      end
    end
    

    For a deeper understanding of blocks, procs, and lambdas, check out the following post: Blocks, Procs, and Lambdas: A Beginner's Guide to Closures and Anonymous Functions in Ruby

    The get method takes a URL path and a block as arguments. The block represents the handler code that should be executed when a request matching that pa

    The Router Class 237 words
  • Move Using the Router
    Open Using the Router

    Let's put the Router class to good use.

    First, we define the routes that our application needs, and then we use the build_response method on the router to generate the HTTP response.

    Let's update the app.rb script as follows:

    # weby/app.rb
    
    require_relative './router'
    
    class App
      attr_reader :router
    
      def initialize
        @router = Router.new
    
        router.get('/') { "Akshay's Blog" }
    
        router.get('/articles') { 'All Articles' }
    
        router.get('/articles/1') { "First Article" }
      end
    
      def call(env)
        headers = { 'Content-Type' => 'text/html' }
    
        response_html = router.build_response(env['REQUEST_PATH'])
    
        [200, headers, [response_html]]
      end
    end
    

    Note that we're setting up the router and initializing the routes in the constructor, and not in the call method. This is important. The constructor is called at the beginning when the application starts, and never again. In contrast, the call method is invoked every time a new re

    Using the Router 302 words
  • Move Step 1: Refactor the Router
    Open Step 1: Refactor the Router

    Let's make a few small changes to the router, so it looks like this.

    # weby/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(path)
        handler = @routes[path] || ->{ "no route found for #{path}" }
        handler.call
      end
    end
    

    Three important things to note:

    1. I've made the Router class a Singleton so we always have a single instance to work with. Refer the Singleton documentation to learn more about how it works. In short, the Singleton pattern ensures that a class has only one globally accessible instance.

    2. Added a `dra

    Step 1: Refactor the Router 177 words
  • Move Step 2: Create the Routes
    Open Step 2: Create the Routes

    In the spirit of Rails, let's create a config/routes.rb file so we have a Rails-like structure. This file contains our application routes.

    # config/routes.rb
    
    require_relative '../router'
    
    Router.draw do
      get('/') { "Akshay's Blog" }
    
      get('/articles') { 'All Articles' }
    
      get('/articles/1') { "First Article" }
    end
    

    Note that we're calling the draw method on the Router and passing a block. Ruby will execute this block (code within do..end above) in the context of the instance of the Router. Hence, the self object in that block will be the Router.

    The above code is similar to:

    router = Router.new
    
    router.get('/') { "Akshay's Blog" }
    
    # and so on...
    

    Don't you just love metaprogramming in Ruby?

    If you're curious to learn more about metaprogramming, check out my summary of Paolo Perrotta's excellent book: Metaprogramming Ruby 2

    There's only one thing remaining. Use th

    Step 2: Create the Routes 132 words
  • Move Step 3: Update the App to Use New Routes
    Open Step 3: Update the App to Use New Routes

    Since we have defined the routes elsewhere, we don't need them in the application constructor. Let's remove them.

    Here's the new app.rb file. Much cleaner, right?

    # weby/app.rb
    
    require_relative './config/routes'
    
    class App
      def call(env)
        headers = { 'Content-Type' => 'text/html' }
    
        response_html = router.build_response(env['REQUEST_PATH'])
    
        [200, headers, [response_html]]
      end
    
      private
        def router
          Router.instance
        end
    end
    

    Note that we're using the Router.instance method added by the Singleton module to get the Router's instance.

    That's it. Restart the application and give it a try. Everything should work as expected.

    We could stop here, but there's one small improvement we can do to make our Router more flexible and powerful.

    Step 3: Update the App to Use New Routes 103 words
  • Move Pass HTTP Environment (Request to Routes)
    Open Pass HTTP Environment (Request to Routes)

    Right now, our routes cannot access the incoming request, i.e. the env hash. It would be really nice if they could use env to add more dynamic behavior. For example, you could access the cookies and session data in your handlers via the HTTP request.

    Let's fix it.

    Before reading the next section, can you try implementing this on your own? We have the env hash in the app.rb. How would you pass it to the handlers?

    All we need to do is pass env to the Router and then further pass it to the handlers when we call it. Here's the changelog.

    # app.rb
    
    response_html = router.build_response(env)
    
    # router.rb
    
    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
    
    # config/routes.rb
    
    get('/articles/1') do |env| 
      puts "Path: #{env['REQUEST_PATH']}"
      "First Article"
    end
    

    If you're curious how the above code works, especially how t

    Pass HTTP Environment (Request to Routes) 189 words
  • Move Let's Implement the Controllers
    Open Let's Implement the Controllers

    Let's Implement the Controllers

    Let's Implement the Controllers
  • Move Introduction
    Open Introduction

    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 lesso

    Introduction 498 words
  • Move What We'll Build
    Open What We'll Build

    We are going to implement a controller structure similar to Rails. By default, Rails stores the controller classes in the controllers directory. We'll do the same.

    Here's an example controller class that returns a string.

    Note: Don't write any code yet, I am only showing what we'll build throughout the lesson

    # controllers/articles_controller.rb
    
    class ArticlesController < ApplicationController
    
      # GET /articles/index
      def index
        'all articles'
      end
    end
    

    It's not exactly similar to a Rails controller, which stores the data in the instance variables. However, it's good enough to keep it simple and explain the fundamentals of controllers. We'll make it more Rails-like later, once we introduce the concept of a model.

    After creating a controller class and the action method, you can define a route to the controller action as follows:

    get 'articles/index'
    

    Whenever the user navigates to the articles/index URL, our application will call the `inde

    What We'll Build 196 words
  • Move Step 1: Create a Controller Class
    Open Step 1: Create a Controller Class

    Create a new controllers directory and add a Controller class called ArticlesController in a file named articles_controller.rb as follows:

    # controllers/articles_controller.rb
    
    class ArticlesController
      attr_reader :env
    
      def initialize(env)
        @env = env
      end
    
      def index
        '<h1>All Articles</h1>'
      end
    end
    
    • The constructor accepts the env hash, storing it in an instance variable, so that all action methods can access it.
    • It contains a single action called index which returns the response.

    In the next lesson, we'll update the router to use this controller.

    Step 1: Create a Controller Class 74 words
  • Move Step 2: Update the Router
    Open Step 2: Update the Router

    Let's modify the router so it recognizes and parses a controller/action route.

    Specifically, we require the articles_controller.rb file and update the get method to handle the new routing syntax, i.e. controller/action while still supporting the older syntax, where we passed the block.

    The following code shows only the changes required to the router.rb file. Specifically,

    1. The get method is updated
    2. Two private methods find_controller_action and constantize are added.
    # router.rb
    
    require_relative 'controllers/articles_controller'
    
    def get(path, &blk)
      if blk
        @routes[path] = blk
      else
        @routes[path] = ->(env) {
          controller_name, action_name = find_controller_action(path)   # 'articles', 'index'
          controller_klass = constantize(controller_name)               # ArticlesController
          controller = controller_klass.new(env)                        # ArticlesController.new(env)
    
          controller.send(action_name.to_sym)                
    
    Step 2: Update the Router 601 words
  • Move Step 3: Add the Route
    Open Step 3: Add the Route

    Finally, update the routes.rb file so our application parses the route and uses the controller action to generate the final view.

    # config/routes.rb
    
    require_relative '../router'
    
    Router.draw do
      get('/') { "Akshay's Blog" }
    
      get '/articles/index'
    end
    

    That's it. We're done. Start the server and navigate to /articles/index path, you should see this:

    article-index.png

    Now we could stop here. However, there's a small refactoring we could do to make the controller look more like Rails.

    Let's extract the constructor to the base class.

    Step 3: Add the Route 80 words
  • Move Refactoring: Extract Constructor to Base Controller
    Open Refactoring: Extract Constructor to Base Controller

    Since all controllers will need a constructor that accepts the env hash, it's better to pull it up in a base class. To stick to the Rails naming conventions, we'll call the base class ApplicationController.

    Add the following code in controllers/application_controller.rb file.

    # controllers/application_controller.rb
    
    class ApplicationController
      attr_reader :env
    
      def initialize(env)
        @env = env
      end
    end
    

    Now our ArticlesController class can extend from this class and we can remove the redundant code.

    # controllers/articles_controller.rb
    
    require_relative 'application_controller'
    
    class ArticlesController < ApplicationController
      def index
        'All Articles'
      end
    end
    

    Restart the application and make sure everything is still working as expected.

    Nice, clean, and tidy. We have a functioning controller structure which puts us well on the path to implementing views and generating dynamic HTML using the ERB gem, which we'll explore i

    Refactoring: Extract Constructor to Base Controller 131 words
  • Move Serving Static Files
    Open Serving Static Files

    Serving Static Files

    Serving Static Files
  • Move Introduction
    Open Introduction

    So far, we have created a simple-yet-complete web application in Ruby without using Rails with the support of routing and controllers.

    However, we are still rendering the contents of the view from our controller classes. For example:

    class ArticlesController < ApplicationController
      def index
        '<h1>All Articles</h1>'
      end
    end
    

    As things stand now, our application mixes the logic and the views together in a single file. Although there's no custom 'application logic' here, you can see the response HTML with h1 tags mixed in the Ruby controller.

    This is 'generally' not considered good practice in software development, as it tightly couples the logic and view together (however, some folks (see: React) may have differing opinions). You can't change one of them without understanding or affecting the other.

    Although it works, it's a good idea to separate the view from the controller. That way, we don't have to change the controller classes when the vi

    Introduction 249 words
  • Move Separate View from Application
    Open Separate View from Application

    Let's separate the view from the application logic by moving the response HTML out of the app.rb to a file named index.html under a newly created views directory.

    <!-- views/index.html -->
    
    <h1>All Articles</h1>
    

    Now update the articles_controller.rb to read the contents of this file to build the response. We will use the File#read method to read the file.

    require_relative 'application_controller'
    
    class ArticlesController < ApplicationController
      def index
        index_file = File.join(Dir.pwd, "views", "index.html")
        File.read(index_file)
      end
    end
    

    A few things to note here:

    • The Dir.pwd method returns the path to the current working directory of this process as a string.
    • The File.join method returns a new string formed by joining the strings using "/".

    There are three benefits to separating the view from the application.

    1. The view gets its own .html or html.erb file with the benefits that come with it, like IntelliSense
    Separate View from Application 208 words
  • Move Make it Pretty with CSS
    Open Make it Pretty with CSS

    Have you noticed that our application is very plain-looking? In this lesson, we will add some style to make it look pretty.

    First, let's standardize the index.html by adding a proper HTML structure.

    <html>
      <head>
        <title>Application</title>
        <link rel="stylesheet" href="/public/style.css">
        <meta charset="utf-8">
      </head>
    
      <body>
        <main>
          <h1>All Articles</h1>
        </main>
      </body>
    </html>
    

    Reloading the browser shouldn't show any difference, except the nice title for the tab.

    title.png

    Also, notice that we've added a link to a public/style.css file under the <head> tag.

    Let's create a new public directory with the following style.css file in it.

    /* weby/public/style.css */
    
    main {
      width: 600px;
      margin: 1em auto;
      font-family: sans-serif;
    }
    

    Now reload the page.

    It's not working! 😣

    Our page looks the same, and none of the styles are getti

    Make it Pretty with CSS 268 words
  • Move Serve Static Files via Middleware
    Open Serve Static Files via Middleware

    When a request for style.css arrives, we want to serve the contents of the style.css file. Additionally, we want to serve style.css as it is, without inserting any dynamic content to it.

    The Rack::Static middleware lets us accomplish this exact use case.

    According to the documentation,

    The Rack::Static middleware intercepts requests for static files (javascript files, images, stylesheets, etc) based on the url prefixes or route mappings passed in the options, and serves them using a Rack::Files object. This allows a Rack stack to serve both static and dynamic content.

    Let's update the config.ru file to include the Rack::Static middleware.

    require 'rack'
    require_relative './app'
    
    # Reload source after change
    use Rack::Reloader, 0
    
    # Serve all requests beginning with /public 
    # from the "public" folder 
    use Rack::Static, urls: ['/public']
    
    run App.new
    

    This tells the static middleware to serve

    Serve Static Files via Middleware 317 words
  • Move Dynamic Views
    Open Dynamic Views

    Dynamic Views

    Dynamic Views
  • Move Introduction
    Open Introduction

    When I first started learning Rails, one of the patterns that I thought was pure 'magic' was how you could access a controller's instance variables in the views.

    # controllers/posts_controller.rb
    class PostsController
      def show
        @post = Post.find(1)
      end
    end
    
    # views/posts/show.html.erb
    <%= @post.title %>
    

    For a long time, I didn't understand how this would work behind the scenes. I tried reading the source code a few times without success. I just wasn't familiar enough with Ruby's advanced metaprogramming techniques.

    If you're feeling the same, worry not. In this lesson, we'll learn one way to implement the Rails views pattern, where a controller's instance variables are accessible in the view, in pure Ruby.

    This lesson won't show how Rails actually implements the views behind the scenes. I'm only trying to mimic the external Rails API, i.e. making controller instance variables available in views.

    In the appendix, we'll learn how Rails views work. However, I sti

    Introduction 330 words
  • Move Understanding the Concept of Binding
    Open Understanding the Concept of Binding

    Goal: To understand the concept of binding in Ruby.

    Binding is an elegant way to access the current scope (variables, methods, and self) in Ruby. Typically, you use it for building view templates and executing strings of Ruby code. The Ruby REPL also makes abundant use of binding.

    The basic idea behind binding is to store the current context in an object for later use. Later, you can execute some code in the context of that binding, using eval.

    A Ruby binding is an instance of the Binding class. It's an object that packages or encapsulates the current scope, allowing you to pass it around in your code.

    Objects of class Binding encapsulate the execution context at some particular place in the code and retain this context for future use. The variables, methods, and value of self that can be accessed in this context are all retained. Binding objects can be created using [Kernel#binding](https://docs.ruby-lang.org/en/3.2/Kernel.

    Understanding the Concept of Binding 519 words
  • Move Using ERB with Binding
    Open Using ERB with Binding

    ERB provides an easy to use but powerful templating system for Ruby. Using ERB, Ruby code can be added to any plain text document for the purposes of generating document information details and/or flow control.

    The following code substitutes variables into a template string with erb. It uses Kernel#binding method to get the current context.

    require 'erb'
    
    name = 'Akshay'
    age = 30
    
    erb = ERB.new 'My name is <%= name %> and I am <%= age %> years old'
    puts erb.result(binding)
    
    # Output:
    #
    # "My name is Akshay and I am 30 years old"
    

    Note the <%= title %> statement. Within an erb template, Ruby code can be included using both <% %> and <%= %> tags.

    The <% %> tags are used to execute Ruby code that does not return anything, such as conditions, loops, or blocks, and the <%= %> tags are used when you want to output the result.

    What's going on in the above example?

    1. After requiring the ERB gem and creating a
    Using ERB with Binding 454 words
  • Move Step 1: Add a Render Method on Controller
    Open Step 1: Add a Render Method on Controller

    Let's require the erb gem and add a new method called render on the ApplicationController class. We're adding it to the base controller class so that all controller classes will inherit it.

    require 'erb'
    
    class ApplicationController
      attr_reader :env
    
      def initialize(env)
        @env = env
      end
    
      # new method
      def render(view_template)
        erb_template = ERB.new File.read(view_template)
        erb_template.result(binding)
      end
    end
    

    The new code does the same thing that we saw in our simplified example earlier. However, there're three important things to note here:

    1. It reads the string template from a file called view_template, which we'll learn more about later.

    2. Then it calls the binding method to get the binding in the context of the controller class, making all the instance variables available to the binding.

    3. Finally, it renders the response by calling the result method on the ERB template and passing the binding.

    If this sounds confusing, d

    Step 1: Add a Render Method on Controller 157 words
  • Move Step 2: Render from Router
    Open Step 2: Render from Router

    We're almost done. The reason we added the render method to the controller class is to call it from the router, once the instance variables are set.

    Modify the router's get method so it looks like this.

    def get(path, &blk)
      if blk
        @routes[path] = blk
      else
        @routes[path] = ->(env) {
          controller_name, action_name = find_controller_action(path)   # 'articles', 'index'
          controller_klass = constantize(controller_name)               # ArticlesController
          controller = controller_klass.new(env)                        # controller = ArticlesController.new(env)
          controller.send(action_name.to_sym)                           # controller.index
    
          controller.render("views/#{controller_name}/#{action_name}.html.erb")
        }
      end
    end
    

    The most important point is that we've separated the action-calling and response-rendering mechanisms.

    Before, we were simply calling the action method on the controller and returning whatever response the

    Step 2: Render from Router 204 words
  • Move Create a View Template
    Open Create a View Template

    Modify the ArticlesController class, so that the index action only sets the instance variables, instead of returning the response.

    class ArticlesController < ApplicationController
      def index
        @title = "Write Software, Well"
        @tagline = "Learn to program Ruby and build webapps with Rails"
      end
    end
    

    Next, create a new articles directory inside the views directory, and add a view template index.html.erb in it. Delete the old views/index.html file.

    <html>
      <head>
        <title>Application</title>
        <link rel="stylesheet" href="/public/style.css">
        <meta charset="utf-8">
      </head>
    
      <body>
        <main>
          <header>
            <h1>
              <%= @title %>
            </h1>
            <p>
              <%= @tagline %>
            </p>
          </header>
    
          <hr>
        </main>
      </body>
    </html>
    

    Don't worry, we'll extract all the layout code in a separate template in a future post.

    To make things pretty, I've updated the style.css as follows:

    Create a View Template 172 words
  • Move Implement Logging
    Open Implement Logging

    Implement Logging

    Implement Logging
  • Move Introduction
    Open Introduction

    Building and deploying an application is only the beginning. You have to maintain it. When something goes wrong, how can you figure out what's the problem?

    Like backups, logging is one of those topics that seems unnecessary until something goes wrong! It's very frustrating to fix a bug that's only reproducible in production without having any logs to help you debug it. This post covers the basics of logging in Ruby and adds logging to our no-rails app.

    It's very easy to find and fix bugs when you're in the development mode. You have the terminal open with the Rails logs scrolling by you. If something goes wrong, just replay it, find the error in the terminal, and fix it.

    But in production (or even staging), you can't just open the terminal and search the endless terminal output. Past a limit, it won't even let you scroll up. You need a better solution. You need to redirect the log streams to a different destination than the standard output to view or archive them, e.g. log files or a 3rd party serv

    Introduction 381 words
  • Move Logging in Ruby
    Open Logging in Ruby

    Normally we can output informational messages using Ruby's puts function. For example, to print a message when the request hits the index action, we could simply print a message as follows.

    class ArticlesController < ApplicationController
      def index
        puts 'displaying all articles'
        @title = 'All Articles'
      end
    end
    

    It outputs the message which by default prints to the standard output, that is, the terminal. However, in production, the log messages will be lost, as we're not saving them anywhere. Hence, we need a flexible solution that can customize the log endpoints based on the environment.

    To get started with logging, we'll use Ruby's logger standard library. Specifically, we'll use the Logger class. From the docs,

    Class Logger provides a simple but sophisticated logging utility that you can use to create one or more event logs for your program. Each such log

    Logging in Ruby 353 words
  • Move Logging to a Log File
    Open Logging to a Log File

    Let's inspect another benefit of the Logger class, the ability to log to a separate log file. Modify our script so it looks like this:

    require 'logger'
    
    logger = Logger.new('development.log')
    
    logger.info 'user logged in'
    logger.error 'could not connect to the database'
    

    Note that instead of using $stdout, we're now passing the name of the log file, development.log. This tells the logger to log all messages to this file instead of the terminal. If the file doesn't exist, it will create one. Let's verify by running the script.

    $ ruby main.rb
    

    No logs were printed to the terminal. However, it should've created a new file called development.log in the current directory, with the following content.

    # Logfile created on 2023-07-29 15:09:08 -0700 by logger.rb/v1.5.3
    I, [2023-07-29T15:09:08.616862 #36690]  INFO -- : user logged in
    E, [2023-07-29T15:09:08.616903 #36690] ERROR -- : could not connect to the database
    

    If you run the program again, t

    Logging to a Log File 176 words
  • Move Log Levels
    Open Log Levels

    A log level is an integer representing the importance or severity of a log event. The higher the level, the more severe the event.

    It's recommended to specify a log level whenever you create a log. Primarily, it's to indicate how serious or important the log message is. It also helps with filtering and finding specific logs from the log files.

    In addition to info and error, the logger provides following levels of logging.

    logger.debug('Maximal debugging info') # log level: 0
    logger.info('Non-error information')   # log level: 1
    logger.warn('Non-error warning')       # log level: 2
    logger.error('Non-fatal error')        # log level: 3
    logger.fatal('Fatal error')            # log level: 4
    logger.unknown('Most severe')          # log level: 5
    

    Behind the scenes, these shorthand methods use logging levels like Logger::DEBUGLogger::WARN, etc.

    When you call any of these methods, the entry may or may not be written to the log, depending on the entry’s severity and on the

    Log Levels 428 words
  • Move Log File Rotation
    Open Log File Rotation

    If you're worried about the log files growing infinitely in size, you can implmement log file rotation. It keeps the log files to a manageable size, and you can either archive or even delete the previous log files, depending on the criticality of your application.

    Retention is often a legal issue, as logs can often contain personal information that's regulated.

    Ruby's logger supports both size-based rotation as well as periodic rotation. Check out the docs to learn more.

    Alright, that's enough information about the logging in Ruby. Let's put the Logger class to good use in our no-rails web application written in Ruby.

    Log File Rotation 99 words
  • Move Adding Logging to Our Ruby App
    Open Adding Logging to Our Ruby App

    We'll start with the app.rb script, which is the entrypoint of our application. First, require the logger library and create an instance of logger in the constructor of the App class. Just like Rails, our logger uses a log/development.log file.

    The logger won't create a log directory for you, so make sure to create it before running the program.

    When the application receives a request, i.e. when the call method is invoked, we'll log a message containing the HTTP request method and the request path.

    require 'logger'
    require_relative 'config/routes'
    
    class App
      attr_reader :logger
    
      def initialize
        @logger = Logger.new('log/development.log')
      end
    
      def call(env)
        logger.info "#{env['REQUEST_METHOD']}: #{env['REQUEST_PATH']}"
    
        headers = { 'Content-Type' => 'text/html' }
    
        response_html = router.build_response(env)
    
        [200, headers, [response_html]]
      end
    
      private
        def router
          Router.instance
        end
    end
    

    Alright... let's

    Adding Logging to Our Ruby App 506 words
  • Move Models and Autoloading with Zeitwerk
    Open Models and Autoloading with Zeitwerk

    Models and Autoloading with Zeitwerk

    Models and Autoloading with Zeitwerk
  • Move Why Use Models?
    Open Why Use Models?

    In this lesson, we will introduce the concept of models in our Ruby web application and then loading them with Zeitwerk.

    Let's revisit the controller class we added in the previous article on controllers.

    # controllers/articles_controller.rb
    
    require_relative "application_controller"
    
    class ArticlesController < ApplicationController
      def index
        @title = "Write Software, Well"
        @tagline = "Learn to program Ruby and build webapps with Rails"
      end
    end
    

    The @title instance variable is accessed in the ERB view as follows:

    <%# views/articles/index.html.erb %>
    
    <header>
      <h1>
        <%= @title %>
      </h1>
      <p>
        <%= @tagline %>
      </p>
    </header>
    

    This works fine, but as the data we want to display gets complicated in size and shape, it doesn't make sense to store it in separate variables. For example, suppose we want to display a post, with its title and body. With separate variables, you might write this:

    Why Use Models? 265 words
  • Move Let's Add Models
    Open Let's Add Models

    Let's create a new model called Article, which is just a plain Ruby class. For this, we'll first create a new directory weby/models and add a article.rb file in it.

    # models/article.rb
    
    class Article
      attr_reader :title, :body
    
      def initialize(title, body)
        @title = title
        @body = body
      end
    end
    

    Next, I'll update the controller so it creates an instance of Article model, passing the title and body properties. Although they're hard-coded for now, we'll fetch them from the database in a future lesson.

    # controllers/articles_controller.rb
    
    require_relative 'application_controller'
    require_relative '../models/article'
    
    class ArticlesController < ApplicationController
      def index
        @title = "Write Software, Well"
        @tagline = "Learn to program Ruby and build webapps with Rails"
    
        @article = Article.new "Rails Companion", "Let's build a web application in Ruby, without Rails!"
      end
    end
    

    Finally, update the index.html.erb t

    Let's Add Models 252 words
  • Move Introduction to Zeitwerk
    Open Introduction to Zeitwerk

    In this lesson, we will use the Zeitwerk gem to load our dependencies without explicitly requiring them.

    First things first: it's Zeitwerk, not Zeitwork. For some reason, I always thought it was Zeitwork. It was only when I tried to install the gem I noticed the typo. Luckily, there's no such gem called Zeitwork.

    In Rails, you don't have to require classes and modules in advance. Somehow, they're just available to use right when you need them. This is all thanks to Zeitwerk, which is written by Xavier Noria, member of the Rails core team.

    Now, you can enjoy the benefits of Zeitwerk without really understanding what it is or how it works, but it's good to know how to use it outside Rails to get the benefits it provides in your Ruby projects. For example, the new Kamal gem uses Zeitwerk internally to load its dependencies.

    Let's get started.

    Introduction to Zeitwerk 147 words
  • Move Using Zeitwerk to Load Constants
    Open Using Zeitwerk to Load Constants

    To load all the dependencies like classes and modules without explicitly requiring them, we'll use Zeitwerk. In the weby directory, run the following command:

    $ bundle add zeitwerk
    

    Next, require the gem in the app.rb file.

    # app.rb
    
    require 'zeitwerk'
    

    Finally, in the application constructor, add the directories where you want to look for constants. I'll add the models and controllers directories, so that Zeitwerk loads the model and controller classes automatically.

    # app.rb
    
    class App
      def initialize
        @logger = Logger.new("log/development.log")
    
        loader = Zeitwerk::Loader.new
        loader.push_dir('models')
        loader.push_dir('controllers')
        loader.setup
      end
    end
    

    P.S. The push_dir method pushes the directory to the list of root directories.

    Now, remove the require_relative statements from the articles_controller.rb and router.rb file that load controllers and models.

    Restart the application, and every

    Using Zeitwerk to Load Constants 146 words
  • Move How Rails Configures Zeitwerk
    Open How Rails Configures Zeitwerk

    If you're curious how Rails uses and configures Zeitwerk, here's a highly simplified version of the Rails codebase that does what we just did.

    The Autoloaders class sets up a Zeitwerk loader. The autoload paths are managed by the Rails.autoloaders.main autoloader.

    # railties/lib/rails/autoloaders.rb
    
    module Rails
      class Autoloaders
        def initialize
          require "zeitwerk"
    
          @main = Zeitwerk::Loader.new
        end
      end
    end
    

    The Finisher module adds an initializer that adds the configured directories to the Zeitwerk loader.

    # railties/lib/rails/application/finisher.rb
    
    module Rails
      class Application
        module Finisher
          initializer :setup_main_autoloader do
            autoloader = Rails.autoloaders.main
    
            autoload_paths.uniq.each do |path|
              autoloader.push_dir(path)
            end
    
            autoloader.setup
          end
        end
      end
    end
    ``
    
    How Rails Configures Zeitwerk 95 words
  • Move Understanding the Concept of a Middleware
    Open Understanding the Concept of a Middleware

    Understanding the Concept of a Middleware

    Understanding the Concept of a Middleware
  • Move Introduction
    Open Introduction

    In this lesson, we'll learn almost everything about Rails middleware: what it is, why we need it, how it works, and why it's so important. We'll also learn how to create and test custom middleware in a Rails app. Finally, we'll extract logging functionality in its own middleware in a simple web app.

    http-request-flow.webp

    In the previous lesson, we added support for logging, and this one shows how to extract the logging functionality to a middleware, to keep our application free from peripheral concerns.

    Most web applications have some functionality that's common for many (or even all) HTTP requests. For example, an application will authenticate the user and log the incoming HTTP request, add new headers, or check if the request is for a static file and serve it from a CDN without hitting the app server.

    Middleware is an elegant way to organize this common functionality, i

    Introduction 314 words
  • Move What is Middlware?
    Open What is Middlware?

    A middleware is nothing but a class with a call method that receives the incoming HTTP request (env) from the web server, processes it before passing it to the Rails application, processes the response received from the application, and returns the response back to the web server.

    When we say an application is Rack-compliant, it means the following:

    1. It has a call method that accepts a single argument env, containing all the data about the request, and

    2. It returns an array containing the status, headers, and response.

    Because the Rack interface is so simple, you can use any code that implements this interface in a Rack application, allowing you to build small, focused, and reusable applications that work together to provide different functionalities. These mini-components are known as Middleware.

    It's best to envision middleware as a series of "layers" HTTP requests must pass through before they hit your application. Each layer can examine the request, modify it, and even re

    What is Middlware? 251 words
  • Move Why Use Middlware?
    Open Why Use Middlware?

    Middleware is very useful for writing logic that is not specific to your web application, such as authenticating the request, logging, or error handling. It focuses on doing one thing and doing it well.

    Middleware sits between the user and the application code. When an HTTP request comes in, the middleware can intercept, examine, and modify it. Similarly, it can examine and modify the HTTP response before forwarding it to the user.

    Middleware provides a convenient mechanism for inspecting and filtering HTTP requests entering your application. For example, Rails includes the Rack::MethodOverride middleware that inspects and overrides the HTTP verb of the incoming request.

    Using middleware simplifies your application code, and it can only focus on the logic related to the application.

    Authentication is another good use case for middleware. If the user is not authenticated, the middleware will redirect the user to your application's login screen. However, if the user is authenticated, the middl

    Why Use Middlware? 353 words
  • Move Let's Add Custom Middleware in Rails
    Open Let's Add Custom Middleware in Rails

    Imagine you want to add a new middleware for your application that does a simple thing: verify a token sent in the header by the client code. If this token matches our secret token, allow the request to proceed. Otherwise, immediately return an error without passing the request to the application.

    To accomplish this, let's introduce a custom middleware, which is just a Ruby class with the interface we saw in the introduction. As mentioned above, it will have a call method that takes the incoming request and returns an array containing the HTTP status, headers, and the response body.

    # lib/middleware/verify_token.rb
    
    module Middleware
      class VerifyToken
        def initialize(app)
          @app = app
        end
    
        def call(env)
          request = ActionDispatch::Request.new(env)
          if request.headers['token'] != 'my-secret-token'
            return [200, {}, ['invalid or missing token']]
          end
    
          @app.call(env)
        end
      end
    end
    

    As you can see, in the call metho

    Let's Add Custom Middleware in Rails 395 words
  • Move Extract Logging Middleware
    Open Extract Logging Middleware

    In the previous lesson, we implemented the logging functionality for our simple web application written in vanilla Ruby.

    However, one thing that's been bothering me is how the logging-related code is scattered throughout our application, i.e. app.rb file.

    # app.rb
    
    require 'debug'
    require 'logger'
    require 'zeitwerk'
    
    require_relative 'config/routes'
    
    class App
      attr_reader :logger
    
      def initialize
        # ...
    
        @logger = Logger.new('log/development.log')
      end
    
      def call(env)
        logger.info "#{env['REQUEST_METHOD']}: #{env['REQUEST_PATH']}"
    
        headers = { 'Content-Type' => 'text/html' }
    
        response_html = router.build_response(env)
    
        [200, headers, [response_html]]
      rescue => e
        logger.error("Oops, something went wrong. Error: #{e.message}")
        [200, headers, ["Error: #{e.message}"]]
      end
    
      private
        # ...
    end
    

    If you remember, whenever the Puma web server receives a new HTTP request, it calls the call method on the App class. Si

    Extract Logging Middleware 469 words