Creating Autoloading In Sinatra

Have you ever wonder why you never needed to require any files or modules in the Rails work we've done so far? This is one of the nicesties of using Ruby on Rails framework: the auto-loading feature.

Rails will magically require and load the necessary files into the runtime when needed preventing the need for explicit require statements. Unfortunately, in pure Ruby no such concept exists, and developers must either specifically require or load files into the Ruby runtime as the application executes.

So are we chained to Ruby's bhavacakra of runtime requirements and loading? Not necessarily, we can write a quick version of our own autoloading that can give us 80% of the results with minimal effort. To do so we will break up the responsibility of 1) loading our gems via Bundler and 2) loading application files. Let's tackle it below:

# app.rb
require 'bundler'
Bundler.require ❶

class App < Sinatra::Base
  set :root, File.dirname(__FILE__) ❷
  require File.join(root, '/config/autoload.rb') ❸

  # ... Rest of application code

In our app.rb file we manually import the bundler gem and call the Bundle.require ❶ method. Once called Bundler will read through our Gemfile and load the listed gems into our runtime environment without us having to require them.

Next, we set a Sinatra configuration setting for the app using the set method. The set method creates an application wide attribute using the a setting name and value as arguments. These values are retrievable from the root class (e.g. App) or settings object for non-class based Sinatra applications. In our case we set the value root ❷ to as a relative file path to our project's root directory.

Once we have the root value set, we manually require a new file called config/autoload.rb ❸. This file will contain logic to load our application specific code into the runtime environment handling the part 2) of our autoloading scheme. Let's explore the file below:

# We manually require various 
# gem submodules here
require 'sinatra/base' ❶
require 'sinatra/reloader' 
require 'sinatra/namespace'
require 'sinatra/activerecord' 

# These are the directories 
# for our application ruby code
directories = %w[config].freeze ❷ 

directories.each do |d| 
  # For each directory we construct a path to 
  # all ruby files (*.rb) in that directory
  ruby_files_in_dir = File.join(d, '*.rb') ❸

  # Using the relative location 
  # we construct a path from the root directory of 
  # the app to load the files
  files_path = File.join(App.root_dir, ruby_files_in_dir) ❹

  # We then iterate through 
  # each ruby file in that directory and require it
  Dir[files_path].sort.each do |f| ❺

    next if f.include?('autoload') ❻ 

    require f ❼

So what is going on here? At the top of the file we are manually requiring a few Sinatra submodules ❶. We've seen the base module in our app.rb file, and will soon be adding  development reloading and route namespacing using the reloader and namespace modules respectively. Requiring these submodules here allows us to clean up the app.rb file significantly and centralize our loading scheme.

Next we set up a list of directories to load files from ❷ (our app being simple at the moment we only have the config directory). The code then iterates through each directory and sets up a relative path to all the ruby files contained within ❸. Using this relative location, we then expand the file path to the project's root directory ❹ using the App.root configuration we set up previously¹. Using this filepath, we then use the directory globbing function Dir[] (the [] method is an alias for glob) to expand the wildcard path (.rb ) and grab all files that match this pattern in that directory. In our case the list of files after globbing with be config/autoload.rb. Once we have an array of Ruby files in the directory we require them one by one ❼ unless the file name is autoload.rb ❻ file which is already been required and therefore should be skipped.

It seems quite complex but it is really just a clever way to enable our laziness. As we add more application code we will return to this file to add new directories causing all the child ruby files to be automagically loaded in. That is surprisingly it for adding basic autoloading to our Sinatra app.

Have fun!

¹ Why are we able to access the App constant in our autoload file? The point is a bit nuanced but important to understand. Recall that our autoloading file is required in the app.rb  file. Therefore, the config/autoload.rb file is executing in the context of the require statement inside the definition of App. Without going too far down the rabbit whole of Ruby constant definitions, the important point to understand here is that the order does matter. We need to require our autoload file after (in our case inside) the initial declaration of our App constant in order for this autoloading scheme to function.