Ruby Web Applications Without Rails Adam Crownoble Nov 6 2015

Like many web developers, I got my start programming on the web with PHP. When you're working with PHP it's obvious how to start from scratch on a simple web app. Get out a .php file, two cups of HTML, a pinch of <?php ?> tags. Throw it in the oven and you're done.

With Ruby, things aren't so obvious. Virtually every Rubyist gets his or her start with a framework. You go along happily pecking out one line of code after another, never understanding what lies beneath the surface.

There are a few reasons you may want to ditch the framework and go bare-bones:

Speed - Rails can't match just Rack for speed. A "Hello World!" in Rails takes about 1.13 seconds to render. A simple Rack application takes < 10 milliseconds. Rails has some tricks to bring that time down on subsequent requests, but from a cold start using shotgun, those are the numbers.

- Rails can't match just Rack for speed. A "Hello World!" in Rails takes about 1.13 seconds to render. A simple Rack application takes < 10 milliseconds. Rails has some tricks to bring that time down on subsequent requests, but from a cold start using shotgun, those are the numbers. Size - Rails also uses a lot more memory than a bare-bones Rack application.

- Rails also uses a lot more memory than a bare-bones Rack application. Simplicity - Rails creates a lot of files to wade through. Sometimes a single config.ru is enough.

- Rails creates a lot of files to wade through. Sometimes a single is enough. Future-Proofing - Rails has a very aggressive release schedule and isn't afraid to deprecate things and leaving vulnerabilities in old versions unpatched. Rack, on the other hand, tends to remain stable for long periods of time.

Don't get me wrong, I love Rails. It's my framework of choice for most of my projects. My rule of thumb is, if it's dirt simple and won't change much, go with just Rack. If it's pretty simple and could change a bit, use Sinatra. Anything else, use Rails. I've started projects saying, "I won't need all the stuff in Rails for this", only to find myself going through the work of adding in most of what Rails would have given me out of the box.

What is Rack

Rack is the standard that all Ruby web servers use to interact with Ruby web applications. Rails is a Rack application. Sinatra is a Rack application. So are Volt, Lotus, Grape and just about everything else.

Rack is both a standard for Ruby web apps and also a gem. It's not necessary to use the gem but it does tend to make life easier.

Hello World With Rack

run -> env { [ 200 , { 'Content-Type' => 'text/plain' }, [ 'Hello World!' ]] }

There it is. That's all you need to have a basic Rack app. Stick it in a config.ru file, run rackup and visit http://localhost:9292 and you should see your first bare-bones Ruby web app.

Now that we know that it works, let's back up and figure out what exactly is going on here. We'll take it step by step.

config.ru - The .ru stands for Rackup file. It's just a Ruby file, but it's designed to be read by a Ruby web server. If you try running ruby config.ru you'll get an error.

- The stands for Rackup file. It's just a Ruby file, but it's designed to be read by a Ruby web server. If you try running you'll get an error. run - This is a method defined by the Ruby web server. It expects a Rack application as an argument.

- This is a method defined by the Ruby web server. It expects a Rack application as an argument. -> - This is called a stabby lambda. It's shorthand for a anonymous function. Everything inside { } will be run when the #call method is called on it.

- This is called a stabby lambda. It's shorthand for a anonymous function. Everything inside will be run when the method is called on it. env - This is an argument for the lambda. Every Rack application must have a #call method that takes one argument. That argument is an environment hash which contains all the information about the HTTP request that was made.

- This is an argument for the lambda. Every Rack application must have a method that takes one argument. That argument is an environment hash which contains all the information about the HTTP request that was made. [] - The return value of the #call method of a Rack app must be an array with three values.

- The return value of the method of a Rack app must be an array with three values. 200 - The first element of the returned array is the status code to be returned.

- The first element of the returned array is the status code to be returned. {'Content-Type' => 'text/plain'} - This is a hash of the headers that will be sent with the HTTP response

- This is a hash of the headers that will be sent with the HTTP response ['Hello World'] - This is the body of the response. This is where HTML would normally go. Notice it's a string wrapped in an array. Whatever object you give here must respond to #each , which a Ruby array does of course.

Making It Do Something

This is all fine and dandy, but what's the point of having Ruby just spit out "Hello World!". Let's make it do something dynamic and useful.

run -> env { [ 200 , { 'Content-Type' => 'text/plain' }, [ "Your IP: #{ env [ 'REMOTE_ADDR' ] } " ]] }

There you have it. You have your very own super basic whatismyip.com clone in one line of code.

Making It Better

The code above works but it's not a practical way to write an actual application. Let's get a better structure going. Something that we can build off of.

class MyApp attr_reader :request def initialize ( request ) @request = request end def status if homepage? 200 else 404 end end def headers { 'Content-Type' => 'text/html' , 'Content-Length' => body . size . to_s } end def body content = if homepage? "Your IP: #{ request . ip } " else "Page Not Found" end layout ( content ) end private def homepage? request . path_info == '/' end def layout ( content ) %{<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Your IP</title> </head> <body> #{content} </body> </html>} end end class MyApp :: Rack def call ( env ) request = Rack :: Request . new ( env ) my_app = MyApp . new ( request ) [ my_app . status , my_app . headers , [ my_app . body ]] end end run MyApp :: Rack . new

So that's a lot of code for an app that doesn't do much, but the point is to see how you can build out classes that feed into a bare-bones Rack app. You can see that a couple features were added like being HTML instead of plain text, and having a working 404 functionality and providing a Content-Length header.

If you'd like to see a real world example of a Rack-only application, check out the small app I wrote to be the backend of the contact form for this site.