Rack provides a modular interface for developing web apps in Ruby. A Rack::Handler
connects webservers with Rack. By default it ships with handlers for Thin, WEBrick, FastCGI, CGI, SCGI and LiteSpeed. It also provides a handy rackup
commandline tool, which internally starts a Rack::Server.
Any object that responds to a call
function and takes an env hash as parameter, returning an Array with the http response code, hash of headers, and a response body that can respond to each
. [1]
For example the below instance of the class RunMe
. You can run this with a Rack Handler.
require 'rack' | |
class RunMe | |
def call(env) | |
code = 200 | |
headers = { 'Content-Type' => 'text/html' } | |
body = ['<h1>Hello Rack!</h1>'] | |
return [code, headers, body] | |
end | |
end | |
app = RunMe.new | |
Rack::Handler::WEBrick.run app |
Save all of this in a server.rb and run it with ruby server.rb
.
The rackup
command line tool can be used, in which case you can forego the explicit use of the WEBrick handler. rackup
internally calls Rack::Server.start
class RunMe | |
def call(env) | |
code = 200 | |
headers = { 'Content-Type' => 'text/html' } | |
body = ['<h1>Hello Rack!</h1>'] | |
return [code, headers, body] | |
end | |
end | |
run RunMe.new | |
## Running ## | |
# Run this by calling the following from console. | |
# | |
# rackup config.ru | |
# | |
## |
Running the rack server in Rails
When rails server
is called the start
method of Rails::Server
is called.
Rails::Server
inherits from Rack::Server
.
Rails::Application
is an class with a call
function and has all the properties of [1]. So if we define a class that inherits from Rails::Application
we can serve it with rackup
.
So lets create a simple Rails::Application
. Create a file named config.ru
as follows:
require 'rails' | |
require "action_controller/railtie" | |
class SingleFile < Rails::Application | |
config.session_store :cookie_store, :key => '_session' | |
config.secret_key_base = '7893aeb3427daf48502ba09ff695da9ceb3c27daf48b0bba09df' | |
Rails.logger = Logger.new($stdout) | |
routes.draw do | |
root to: proc {|env| [200, {}, ["Hello world"]] } | |
end | |
end | |
run SingleFile |
This just prints ‘Hello World’. For a more useful experiment, you can define a controller and render a template.
Lets create a PagesController
that renders some html inline.
require 'rails' | |
require 'action_controller/railtie' | |
class SingleFile < Rails::Application | |
config.session_store :cookie_store, :key => '_session' | |
config.secret_key_base = '7893aeb3427daf48502ba09ff695da9ceb3c27daf48b0bba09df' | |
Rails.logger = Logger.new($stdout) | |
end | |
class PagesController < ActionController::Base | |
def index | |
render inline: "<h1>Hello World!</h1> <p>I'm just a single file Rails application</p>" | |
end | |
end | |
SingleFile.routes.draw do | |
root to: "pages#index" | |
end | |
run SingleFile |
Tada! This right here, is a Rails application in a single file, with just 20 lines of Ruby.
For a more complex example, let split out the files. Its not single file anymore!
Lets split out the config.ru
file into application.rb (which handles the Rails part) and config.ru (which handles the run
). And connect to a database(sqlite3) and render a template view file. So now we have a views
folder with users/index.html.erb
in it.
The code is as follows:
require 'rails' | |
require 'active_support/railtie' | |
require "action_controller/railtie" | |
require 'active_record' | |
class SingleFile < Rails::Application | |
config.session_store :cookie_store, :key => '_session' | |
config.secret_key_base = '7893aeb3427daf48502ba09ff695da9ceb3c27daf48b0bba09df' | |
Rails.logger = Logger.new($stdout) | |
end | |
# Define the Model | |
class User < ActiveRecord::Base | |
end | |
# Define the Controller | |
class UsersController < ActionController::Base | |
prepend_view_path 'views' | |
def index | |
Rails.logger.info view_paths | |
@users = User.all | |
end | |
end | |
# Define routes | |
SingleFile.routes.draw do | |
root to: 'users#index' | |
end | |
################### | |
# Some setup to connect to Sqlite3 DB | |
################### | |
def setup_db | |
require 'sqlite3' | |
sqlite_db = File.join(File.dirname(__FILE__), 'test.sqlite3') | |
db = SQLite3::Database.new( sqlite_db ) | |
db.execute( "DROP TABLE users;" ) rescue nil | |
# Create the table | |
db.execute( "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, email VARCHAR(100), name VARCHAR(100));" ) | |
# Lets populate some seed data | |
db.execute( "INSERT INTO users VALUES(1, 'john@example.com', 'John Doe');" ) | |
db.execute( "INSERT INTO users VALUES(2, 'jane@example.com', 'Jane Smith');" ) | |
ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => sqlite_db) | |
end | |
# Run DB setup | |
setup_db |
require_relative './application' | |
run SingleFile |
All the source code for this post is available at this Github Repo.
Although this is just a few files, it loads a lot of stuff internally. It’s definitely possible to remove some things. I’ll explore on how to render a json API and trim down the stack, but that’s for another post.
🤟