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.
Frequently Asked Questions (FAQs) about Rack-App
What is Rack-App and how does it differ from other web microframeworks?
Rack-App is a performant and pragmatic web microframework that is designed to simplify the process of building web applications. Unlike other web microframeworks, Rack-App is built on the Rack library, which provides a minimal interface between webservers and Ruby frameworks. This means that Rack-App can be used with any Rack-compatible server, providing greater flexibility and compatibility than other frameworks. Additionally, Rack-App is designed to be lightweight and efficient, with a focus on performance and simplicity.
How do I install and set up Rack-App?
Installing and setting up Rack-App is a straightforward process. First, you need to install the Rack-App gem by running the command gem install rack-app
. Once the gem is installed, you can create a new Rack-App application by creating a new Ruby file and requiring the Rack-App gem. From there, you can define your application’s routes and start the server.
Can I use Rack-App with other Ruby frameworks?
Yes, Rack-App can be used with any Ruby framework that supports Rack. This includes popular frameworks like Rails and Sinatra. By using Rack-App with these frameworks, you can take advantage of Rack-App’s performance and simplicity while still leveraging the features and functionality of your chosen framework.
How does Rack-App handle routing?
Rack-App uses a simple and intuitive routing system. Routes are defined in your application’s Ruby file using the get
, post
, put
, delete
, and patch
methods. Each method takes a string representing the route’s path and a block that is executed when the route is matched. This makes it easy to define complex routes and handle different types of HTTP requests.
What are the performance benefits of using Rack-App?
Rack-App is designed to be lightweight and efficient, which can lead to significant performance benefits. Because it is built on the Rack library, Rack-App has a minimal footprint and can run on any Rack-compatible server. This means that it can handle a large number of requests with minimal resource usage. Additionally, Rack-App’s simple and intuitive design makes it easy to write efficient and performant code.
How can I handle errors in Rack-App?
Rack-App provides a simple and flexible error handling system. You can define custom error handlers for different types of errors, allowing you to control how your application responds to different error conditions. Additionally, Rack-App’s error handling system integrates seamlessly with Rack’s middleware, allowing you to use middleware to handle errors at different points in your application’s request/response cycle.
Can I use Rack-App for building APIs?
Yes, Rack-App is an excellent choice for building APIs. Its simple and intuitive design makes it easy to define routes and handle different types of HTTP requests, which are key requirements for any API. Additionally, Rack-App’s support for Rack middleware makes it easy to add functionality like authentication, caching, and compression to your API.
How can I test my Rack-App application?
Testing a Rack-App application is similar to testing any other Ruby application. You can use any Ruby testing framework, such as RSpec or Minitest, to write tests for your application. Additionally, Rack-App provides a test helper that makes it easy to test your application’s routes and responses.
Can I deploy my Rack-App application to a cloud provider?
Yes, you can deploy your Rack-App application to any cloud provider that supports Ruby applications. This includes popular providers like Heroku, AWS, and Google Cloud. Because Rack-App is built on the Rack library, it can run on any Rack-compatible server, making it easy to deploy your application to a wide range of environments.
How can I contribute to the Rack-App project?
The Rack-App project is open source and welcomes contributions from the community. You can contribute by submitting bug reports, proposing new features, or submitting pull requests with code changes. All contributions should follow the project’s contribution guidelines, which can be found on the project’s GitHub page.
David Bush is a Web Developer who travels the world and writes code. His favourite languages are Clojure, Ruby and Python. He enjoys learning new technologies, beer, good food and trying new things. www.david-bush.co.uk