Building a Web Application Without Rails : Project Setup

June 18, 2022   • no-rails-app

This is the first post in the series on building a database-backed web application without using Rails. In this post, I will setup the project and create a very simple Rack application running on Puma app server. I will also show how to use Rack middleware to reload the application whenever the source code changes.

Ruby on Rails

I hope that in the process, we all will learn more about how web applications work and get a big-picture understanding of the web frameworks, especially Ruby on Rails. As a side-effect, we will learn a lot of meta-programming in Ruby.

Note: Building applications for the web is like those ‘easy to learn, hard to master’ games. Though it’s very easy to get started, the thread goes deep, and it can take a lifetime to learn and master the various technologies that power a web application. I will try to keep it very simple so we can remain focused on the fundamentals.

So, let’s get started. If you prefer to watch a video instead, scroll to the bottom.


Let’s start by creating a new directory named web.

mkdir web
➜  cd web

Set up Bundler

Bundler makes sure Ruby applications run the same code on every machine.

It does this by managing the gems that the application depends on. Given a list of gems, it can automatically download and install those gems, as well as any other gems needed by the gems that are listed.

Initialize bundler for your project by running the following commands from the project root directory.

➜  gem install bundler
➜  bundle init

It will create a Gemfile in the project. Whenever we want to use a gem in our project, we will add it to this file, and run bundle install command to install it. As a shortcut, bundler provides the bundle add command which adds the gem to the Gemfile and runs the bundle install for us.

Install Puma

To run our web application, we will need an application server. Rails recommends the Puma server and I will use the same.

Use bundle to install Puma.

bundle add puma

Install Rack

All Ruby web servers and frameworks follow the Rack specification. To learn more about Rack, check out my detailed post on it.

bundle add rack

Create Web Application

Add a config.ru file in the web directory.

touch config.ru

Add the following code in it. Rack provides a very simple interface using which web servers communicate with the applications.

  • the application should have a call method that takes the env object representing the HTTP environment
  • returns an array containing the status, headers, and response.

Let’s create an application that follows the above specification.

class App
  def call(env)
    headers = {
      'Content-Type' => 'text/html'
    }
    
    response = ['<h1>Greetings from Rack!!</h1>']
    
    [200, headers, response]
  end
end

run App.new

To launch the web server, run the puma command from the web directory.

➜  puma
Puma starting in single mode...
* Puma version: 5.6.4 (ruby 3.1.0-p0) ("Birdie's Version")
*  Min threads: 0
*  Max threads: 5
*  Environment: development
*          PID: 30031
* Listening on http://127.0.0.1:9292
* Listening on http://[::1]:9292
Use Ctrl-C to stop
127.0.0.1 - - [25/May/2022:17:01:45 -0700] "GET / HTTP/1.1" 200 30 0.0038
127.0.0.1 - - [25/May/2022:17:01:45 -0700] "GET /favicon.ico HTTP/1.1" 200 30 0.0011

Now our web server is running and serving our application.

If you make a change in your application, it won’t be reflected in the browser. For this, you need to restart the server by pressing ctrl + c on the keyboard. After making a change, restart the server by running rackup again. Your change will show up now.

It can get tedious to restart the server after every change. Is there a better way? Turns out, there is. Rack ships with a Rack::Reloader middleware that reloads the application after changing the source code.

To understand how Rack middleware works, again, check out my post on Rack.

Use Rack Reloader Middleware

First, move the contents of the config.ru file to another file named app.rb in the same directory.

# app.rb

class App
  def call(env)
    headers = {
      'Content-Type' => 'text/html'
    }

    response = ['<h1>Greetings from Rack, hello</h1>']

    [200, headers, response]
  end
end

Now replace the contents of config.ru file with the following code:

# config.ru

require 'rack'
require_relative './app'

use Rack::Reloader, 0
run App.new

Launch the web server using puma command, and now our middleware will automatically reload the application after we make a change in the source code. So we don’t have to restart the server. Pretty cool.

Cleanup

Currently, whenever we reload the page, the browser makes two HTTP requests. One for the HTML page and the other for the favicon.ico image, which shows up in the tab.

You can verify this in the console logs:

127.0.0.1 - - [26/May/2022:15:31:18 -0700] "GET / HTTP/1.1" 200 30 0.0087
127.0.0.1 - - [26/May/2022:15:31:18 -0700] "GET /favicon.ico HTTP/1.1" 200 30 0.0055

Our application is responding with the same response for both the requests, which doesn’t make sense. Let’s modify the request to return an empty string for now for the favicon request.

For this, we need to figure out how to differentiate the path for both the requests. This information is contained in the env hash, which represents the HTTP request environment. Let’s modify our code to see what this object looks like. We will use the json gem to return the JSON representing this object.

require 'json'

class App
  def call(env)
    headers = {
      'Content-Type' => 'text/json'
    }

    response = ['<h1>Greetings from Rack!!</h1>']

    [200, headers, [env.to_json]]
  end
end

This renders the following page, when you reload the browser.

env hash

This was the response for the home page. However, if we inspect the response for the favicon.ico request in the developer tools window, it looks like this:

Path info for favicon

That means we can use the PATH_INFO key on the env hash to find out if the request is for the favicon. For now, we will just return a plain string in the response.

class App
  def call(env)
    headers = {
      'Content-Type' => 'text/html'
    }

    return [200, headers, ['favicon.ico']] if env["PATH_INFO"] == '/favicon.ico' 

    response = ['<h1>Greetings from Rack!!</h1>']

    [200, headers, response]
  end
end

In the next post, we will refactor our application to a more Rails-like structure. Stay tuned!!