Rack-App: A Performant and Pragmatic Web Microframework

David Bush
Share

Screen Shot 2016-11-10 at 12.48.47 PM

It is a fantastic time to be a web developer and to use Ruby. Ruby on Rails paved the way for modern Web Development, but in doing so highlighted certain shortcomings. Its “kitchen-sink” approach can sometimes be overkill, particularly for small projects, which led to the proliferation and arguably a golden age of Ruby microframeworks. The success of Sinatra shows that there is a genuine demand for it and its ilk, and the number of them is increasing every few months.

Why are there so many frameworks? Partly because the wonderful Rack makes it incredibly easy for anyone with a basic grasp of TCP/IP to roll their own framework, and partly because by definition, microframeworks are opinionated. The result is that these opinions lead to unscratched itches, which, when combined with a low barrier to entry, has resulted in a plethora of microframeworks on the market. Why would we need another one? Well, we don’t. At this point, pretty well every modern web development use case is catered for by something that exists. So why would anyone be interested in Rack-App? Well, that one is easy. This is the framework that powers microservices at Heroku.

Core Principles

Rack is opinionated. These are the highlights:

  • No metaprogramming – This isn’t a framework that is going to hold your hand, but it won’t give you any untoward surprises either.
  • Performant – we’ll get into this in a moment, but this framework scales.
  • Simplicity – Code bloat? Dependencies? Not here.
  • Emphasis on testing – Rack favours Behaviour Driven Development (BDD) and prides itself on being simple to integrate.
  • Modular – Rack-App provides you with the bare minimum to get started. There are plugins available, but not included.

Sound appealing? Let’s look at what really differentiates it from anything else out there right now: Performance.

Performance

This is a framework that is so concerned about performance it has a separate repository benchmarking itself against all of the others. This is used for catching regressions between versions and includes Rails, Sinatra, and a host of other lesser known frameworks. Also, this is prominently featured on the project’s homepage; this openness is so refreshing (but also probably pretty easy when you’re topping the rankings so consistently!) in a time where we’re frequently trying to eke out every last bit of performance from a tool at scale.

This isn’t a killer feature, though. A killer feature is Rack-App will comfortably serve over 10,000 endpoints (as many as you can fit into memory) with a constant time lookup. Let’s sidetrack a moment to see how it achieves that kind of performance.

Constant Time Lookup

Let’s imagine we have over 10,000 endpoints. How is it possible that lookup time is constant? Well, let’s take a look at the source. We know that this is likely to be related to routing, so take a quick peek at the router here. You can see it’s using our old friend the hash, which, as we all now know, has a constant lookup time. Not quite the ‘null lookup time’ purported, but nonetheless, impressive when serving that many endpoints.

What’s also interesting is that you can namespace endpoints. This means that you’d have to work pretty hard to have too many endpoints, but your code will naturally be DRY (unless you work against it) and still benefit from the rapid lookup times.

Simplicity

One of the benefits of so many Ruby Frameworks is that we’ve seen what works and what doesn’t. When was the last time you had some arcane syntax or messy API in a Ruby framework? Rack-App is no exception. The DSL borrows heavily from (the excellent) Sinatra (and also Grape), which means that new users should feel right at home with a familiar and terse syntax. Best of all, by lowering the barrier to entry in this fashion, there’s really no excuse not to spend a couple of hours picking up this framework to add to your programming arsenal.

Rack embodies ‘The Principle of Least Astonishment‘ and this is a double edged sword. On the one hand, it’s nice not to have to wade through a dozen levels on the stack to work out why something isn’t behaving as it should because someone cleverly overloaded method_missing in an obscure class somewhere. On the other hand, if you make a mistake, nothing is there to catch you. Personally, I find this refreshing. I like my frameworks to treat me as an adult and blow up when I make a mistake. This isn’t for everyone, however, and if this lack of safety net presents an issue with running in production, then there are other options out there.

Testing

To guard against exceptions in the wild, Rack-App (like Rails) extols testing. Behaviour Driven Development (BDD) is the weapon of choice, and a test module comes bundled with Rack. The framework is fully tested and therefore theoretically straightforward to add integration testing to your app – simply require the module in your specification.

In Action

That’s enough about its strengths; let’s try some code!

You know the drill:

gem install rack-app

Here’s the output:

Fetching: rack-2.0.1.gem (100%)
Successfully installed rack-2.0.1
Fetching: rack-app-5.5.1.gem (100%)
Successfully installed rack-app-5.5.1
Parsing documentation for rack-2.0.1
Installing ri documentation for rack-2.0.1
Parsing documentation for rack-app-5.5.1
Installing ri documentation for rack-app-5.5.1
Done installing documentation for rack, rack-app after 4 seconds
2 gems installed

One dependency: Rack. They’re really not kidding when they say they’re light on dependencies.

Let’s knock up a quick ‘Hello World’:

# config.ru
require 'rack/app'

class Racko < Rack::App
  get '/' do
    "Hello World!"
  end
end

run Racko

Run config.ru with a server (I favour the excellent Shotgun because of the live reload facility, but rackup config.ru will work just fine) and browse to localhost:9393 or localhost:9292, depending on your choice. You should see your greeting:

Hello World!

Nothing remarkable so far. Class inheritance, a DSL syntax borrowing heavily from Sinatra and a call to Rack’s run hook. Let’s dig a little deeper.

A Sample Application

Let’s step things up a bit. A more substantial example can be drawn from the project’s homepage. For those of you playing along at home, create the following files: config.ru, mediafileserver.rb and file_uploader.rb. For the lazy:

wget {https://gist.githubusercontent.com/adamluzsi/badf3ac5d40db335b45972aca4b30cd8/raw/cad2e1b137f94cc1217348283b0058524fc71bbc/config.ru,https://gist.githubusercontent.com/adamluzsi/badf3ac5d40db335b45972aca4b30cd8/raw/cad2e1b137f94cc1217348283b0058524fc71bbc/media_file_server.rb,https://gist.githubusercontent.com/adamluzsi/badf3ac5d40db335b45972aca4b30cd8/raw/cad2e1b137f94cc1217348283b0058524fc71bbc/uploader.rb}

Here are the files inline:

# config.ru
require 'json'
require 'rack/app'
class MyApp < Rack::App

  headers 'Access-Control-Allow-Origin' => '*',
          'Access-Control-Expose-Headers' => 'X-My-Custom-Header, X-Another-Custom-Header'

  serializer do |obj|
    if obj.is_a?(String)
      obj
    else
      JSON.dump(obj)
    end
  end

  error StandardError, NoMethodError do |ex|
    { error: ex.message }
  end

  get '/bad/endpoint' do
    no_method_error_here
  end

  desc 'hello world endpoint'
  validate_params do
    required 'words', class: Array, of: String,
                      desc: 'words that will be joined with space',
                      example: %w(dog cat)
    required 'to', class: String,
                   desc: 'the subject of the conversation'
  end
  get '/validated' do
    return "Hello #{validated_params['to']}: #{validated_params['words'].join(' ')}"
  end

  get '/' do
    { hello: 'world' }
  end

  mount MediaFileServer, to: "/assets"
  mount Uploader, to: '/upload'

end

# for more check out how-to
run MyApp

# media_file_server.rb
class MediaFileServer < Rack::App

  serve_files_from '/folder/from/project/root', to: '/files'

  get '/' do
    serve_file 'custom_file_path_to_stream_back'
  end
end

# uploader.rb
require 'fileutils'
class Uploader < Rack::App

  post '/to_stream' do
    payload_stream do |string_chunk|
      # do some work
    end
  end

  post '/upload_file' do
    file_path = Rack::App::Utils.pwd('/upliads', params['user_id'], params['file_name'])
    FileUtils.mkdir_p(file_path)
    payload_to_file(file_path)
  end

  post '/memory_buffered_payload' do
    payload #> request payload string
  end

end

It’s an exercise left to the reader to play around with it, but let’s take a quick look at config.ru where the meat of the interesting parts can be found:
– Headers set as a hash – this is clearly a framework geared towards API usage.
– A serializer – this captures Rack-App beautifully: if you need something, you’d better be prepared to roll your own.
– Errors – Rack-App prides itself on a unified error handling interface; nothing fancy, but you’re not dealing with too much abstraction either.

Conclusion

Rack-App is in its early stages. It’s already bumped up against a little controversy and changes are being made all the time. One arguable shortcoming is the name is near impossible to Google. Fortunately, the documentation is fantastic and the homepage has a HOW-TO menu that links to examples of plenty of common use cases. Personally I appreciate the laconic style, particularly coming from Python’s verbose equivalent because it’s easy to get productive quickly.

Finally, it is important to note that microframeworks, by eschewing dependencies, allow for a greater variance in ways of doing things. If you’re used to Rails’ Convention over Configuration and kitchen-sink approach, then you need to be warned that there is a trade-off in terms of long term maintainability. That said, the benefits of microframeworks are numerous and the demand isn’t going away anytime soon. If you need a performant and battle-hardened backend for serving API endpoints, all while hitting a reasonable level of productivity quickly, Rack-App, to my mind, is the only real choice.

CSS Master, 3rd Edition