Where Ruby/Sinatra falls short

The PC hardware recommender pc-kombo is written with Ruby and Sinatra, a very comfortable and powerful small framework to build web applications. Web applications is where Ruby shines in general, the language got popular together with the more complete (and opinionated) Ruby on Rails framework.

Sinatra is very nice to work with initially, but over the years it became clear to me that not everything works as good as it should. I’d still recommend it, but would urge beginners to be aware of its limitations.

Some of Sinatra’s issues


1. The routing order matters, and evaluation is not always obvious

That’s stated in the documentation:

Routes are matched in the order they are defined. The first route that matches the request is invoked.

Sounds straightforward, but in practice this gets complicated. As soon as you have routes that match by regexpressions, you won’t always know which order is the correct one. This lead to several situations where we had to shuffle the routes around in our server.rb, where the routes are defined.

Worse: The evaluations order changes over time. Whether it was from Sinatra or Ruby version mismatches, several times everything worked as expected on the dev platform, while as soon as the new routes were added to the production server they covered other, more important routes. Something changed the evaluation order, which lead to frantically searching the right position to define the route. Now imagine not having a classical style Sinatra app, but a modular one.

And a small issue on top of that: If you define /route, /route/ will still throw a 404. The right solution would be to serve the page on the two routes but set one as canonical source. But you have to do that manually.

2. How do you start this thing?

Have a look at the readme. It gives a definite answer on how to start Sinatra:

ruby myapp.rb

Now, isn’t it surprising that this is not how I start my Sinatra application? I have to run

bundle exec puma -e development

The bundle exec is not the fault of Sinatra, that’s just a wrapper to solve dependency management. But the puma -e development should be surprising, as should be that I had to define a config.ru. That files gets mentioned in the Readme, but what it is for does not get explained.

Sinatra is part of a broader Ruby web ecosystem around the Rack web environment and with influences from Rails. Ever so often parts from there will become relevant to you when you use Sinatra, but if you don’t already know all about Rack and Rails, then you’ll be lost until figuring this ecosystem out.

3. The application webserver story is strong but confusing

So apart from the problem with starting it, what exactly did it start? Sinatra by default can run with multiple application servers. By default is searches for thin, mongrel or webrick, in this order. But maybe you want to run a different server? The start command above shows I use puma. But there is also unicorn, passenger, falcon, and probably many more. It is nice to have many good options, but which one is right for you? Which one can scale the incoming requests properly, ideally over multiple cores?

And besides: If Sinatra starts a server listening for incoming traffic, why does it still seem common to run a regular webserver like nginx in front of that server? Wouldn’t it be better to map port 80 to the application server port with a firewall rule and serve the application directly? Will the url rewriting used in the template understand your setup?

All those questions are not properly answered by the documentation, and you are on your own to find a setup that works for you.

4. There are too many ways to access parameters

The documentation shows this well. There are many ways to react to information given as part of the url or as POST/GET parameter. Let’s count, citing from the Readme again. First there are named parameters, accessible via the params hash:

get '/hello/:name' do # matches "GET /hello/foo" and "GET /hello/bar" # params['name'] is 'foo' or 'bar' "Hello #{params['name']}!"

Or you can put them into a block:

get '/hello/:name' do |n| # matches "GET /hello/foo" and "GET /hello/bar" # params['name'] is 'foo' or 'bar' # n stores params['name'] "Hello #{n}!"

You can also use wildcards with params['splat']:

get '/say/*/to/*' do # matches /say/hello/to/world params['splat'] # => ["hello", "world"]
end get '/download/*.*' do # matches /download/path/to/file.xml params['splat'] # => ["path/to/file", "xml"]

Or they can be blocks:

get '/download/*.*' do |path, ext| [path, ext] # => ["path/to/file", "xml"]

Or use a regexpression with params['captures']:

get /\/hello\/([\w]+)/ do "Hello, #{params['captures'].first}!"

And those can also be blocks:

get %r{/hello/([\w]+)} do |c| # Matches "GET /meta/hello/world", "GET /hello/world/1234" etc. "Hello, #{c}!"

And that’s not all, there are still optional parameters, and params also contains the regular GET and POST parameters like ?param1=abc&param2=def (by the way: what happens when you send an URL like that as a POST method?). So let’s stop counting here, there is no clear number.

I get that Sinatra wants to be unopinionated and provide several good solutions, but a range of options this broad just leads to chaos.

5. You will need much more than included

Some common scenarios I guarantee you will run into:

  1. HTML-escaping a string in your template
  2. Serving a translated page for visitors from other countries
  3. Rewriting www.yourpage.com to yourpage.com, or the other way around
  4. Serving your site over https
  5. Redirecting all http:// pages to https://
  6. Running tasks in the background
  7. Saving data in a database, and reading from it

All of that you will have to solve outside of Sinatra, with gems like rack-rewrite (using the config.ru from above), picking from Rack::Utils or using sanitize, concepts like threadpools or queues, maybe using additional programs like Redis.

Not all of that is bad: Having to solve database access on your own leads naturally to writing raw SQL, which is often the best solution anyway. But easy problems like escaping HTML get surprisingly hard when your framework does not provide it and you have to figure out how to transport the Rails-centric solutions you find online to your Sinatra program.

6. Which helpers are available anyway?

Sinatra has a lot of useful stuff included, but you have to discover it. And the only place for that seems to be the Readme. This can be slightly problematic. For example: Writing urls in the template often profits from not hardcoding your url, and a gem like url_for can do that. I was still using an external solution way past the moment Sinatra included its own url method, because I was simply not aware of that.

There are solutions for mime types, cache control, redirects, logging, sending files, dealing with dates and some more. And in sinatra-contrib there is some more specialized stuff that will be less often needed. But all that means that you have to think at the right moment to check the Readme, because since not everything is included, sometimes you will miss that a solution to your specific problem is already there.

7. The issue with sessions

Sessions are great, but we ran into overusing them. And there are several per-requisites to make them work. It starts with security:

For better security and usability it’s recommended that you generate a secure random secret and store it in an environment variable on each host running your application so that all of your application instances will share the same secret. You should periodically rotate this session secret to a new value. Here are some examples of how you might create a 64 byte secret and set it

and goes on to realizing you have to use Rack::Session::Pool to be able to store more than the simple values (by storing them on the servers instead of in the visitors cookie). And finally it is being quite complicated to not have multiple session middlewares running by just following the code in the Readme.

There are also not that many alternatives, not many session middlewares you can actually use. There basically is moneta, and while a great project I did not see many reports of the advantages - apart from storing session in a SQL database and thus making them persistent over application restarts while keeping the flexibility of Rack::Session::Pool of what can be stored.

But our main problem was and is overusing sessions. In transporting data via sessions it is too easy to create an application where sharing an url will not lead to the second person seeing the same content on the shared url. That’s why we need a share button to create a permalink after creating a PC build. Now, that’s not really the fault of Sinatra – it’s an architectural issue. But shouldn’t it be the job of the web framework to provide a better alternative? A way to map application content to urls where a part of the url changes with the content? Maybe that is just too specific to pc-kombo, but something to keep in mind when building a similar dynamic application.

8. The built-in protection will block valid access patterns

This was a bad one. Sinatra uses Rack::Protection to prevent a whole bunch of typical attacks. But that way it also blocks valid access patterns, like transporting the jwt access token from the portier authentication system. And then basically the worst case happened in a different project: An early launch that got a lot more publicity than expected with a bunch of visitors not being able to login, because the protection blocked the access token from all visitors using Chrome. Just the most popular browser out there. I had to disable parts of that to make it work, which kind of defeats the purpose of having the protection built in.

9. And finally: Is Ruby the right language for you?

If you start with Sinatra, maybe you do want to start with a cheap server. Who knows whether the new application will earn any money, or maybe earning money is not even the goal. A good way could be a low-power multi-core server like the ones provided by Scaleway, 4 ARM cores for cheap are a good fit for many web applications. Or maybe you know you will get many visitors and want something as efficient as possible.

In that case, at first glance Sinatra is not a bad fit. Compared to Rails it is very lightweight, which has a positive effect on performance.

But the problem might be Ruby. If you use the regular C-Ruby, you use a language that has a very prominent Global Interpreter Lock, and that GIL basically means that your app will be very bad in using multiple small cores. Depending on where you want to deploy your webapp and how many visitors it will serve that might be a problem.

It is still a nice solution

I don’t want to end that article without saying something positive. My true point is: Do not evade Sinatra, but be aware of its shortcomings before starting a project with it.

While the above focuses on Sinatra’s shortcomings, that does not mean that I think it is a bad solution. To the contrary: There are not many web frameworks out there that enable such a nice and fast initial developer experience. With its DSL approach and by providing quite a bunch of solutions to common problems Sinatra is still a great tool for making web apps that work and that can get developed fast. It is even better for just providing an API, or for a very focused web app. It just gets less nice when you realize that the moment you chose Sinatra you also put yourself in a situation where you later will definitely be in uncharted territory.

After you solved those issues once you will have your toolset that covers at least most of those typical issues as well. And you will be more capable for it, regardless with which language and framework you develop your next web application. But till then there will be a lot of uncertainty whether you can succeed with your current Sinatra project at all. And all the time you will find search results showing how to do it in Rails, or with a different more complete framework in a different language.

That’s exciting for some, but not right for every project and not something every developer will want.