Intro to Sinatra
Intro to Sinatra
Learning goals
- Understand the advantages of using a lightweight DSL/framework
- Build a Sinatra application
Warm up
- Why do we use Rails? How do we know when we need it?
- What are some ideas you’ve had for microservices (in Consultancy, or in general)?
- Could you build any of these without using Rails? Why or why not?
What if I want to build a (micro)service without the complexity of Rails?
Ruby on Rails generates a lot of things we might not need for simple web apps. All of that boilerplate code has performance implications, as well as introducing a level of complexity we might not want. Other than Rails, there are many Ruby frameworks and DSLs out there, such as Trailblazer and Hanami.
For now, let’s take a closer look at a very popular choice: Sinatra.
Sinatra
When people speak of Ruby web development, it has historically been in reference to the opinionated juggernaut that is Rails. This is certainly not an unfounded association; Hulu, Yellow Pages, Twitter, and countless others have relied on Rails to power their (often massive) web presences, and Rails facilitates that process with zeal.
Rails was a breath of fresh air to many developers exhausted by the “old ways”; Sinatra enters the arena with a similar game-changer: a beautifully minimalistic, “I’ll get out of your way” approach. No generators, no complex folder hierarchies, and a brief yet expressive syntax that maps closely to the functionality exposed by the Hypertext Transfer Protocol verbs.
— Sinatra Up and Running, Alan Harris & Konstantin Haase
Sinatra is minimalistic and tries to stay out of our way, which sounds great, but what does that look like in practice?
Pick one of the projects from the following list, and spend about 10-15 minutes reading the source code on GitHub.
Then, discuss the following questions with a study partner:
- What did you notice about the project file trees?
- How does routing work in Sinatra? How does this compare to a Rails
routes.rb
file? - Is there any syntax that feels new/unfamiliar?
- What core pieces of functionality do you think Sinatra provides?
Practice: building a Sinatra application
It’s important to note that Sinatra is not a web framework, but rather, a DSL (Domain-Specific Language). Web frameworks tend to enforce a design pattern such as MVC, and include an object-relational mapper. Sinatra does neither of these– in fact, it’s possible to encapsulate an entire Sinatra app in a single file.
To familiarize ourselves with this DSL, we’re going to build a basic server. Let’s say we’ve built a Rails app for a local pizza shop. This application provides a variety of features such as generating topping suggestions, order placement, payment processing, a ‘contact us’ form, and more. It’s also scaling rapidly because our clients are so good at making pizza!
We’ve been tasked with restructuring this monolithic application into a Service-Oriented Architechture, and the first step is building a microservice to handle the task of suggesting pizza topping combinations.
First we’ll need a directory to hold all of our Sinatra code, and a file for our server.
mkdir pizza_suggestions
cd pizza_suggestions
touch server.rb
We’ll require the Sinatra gem at the top of our server file, so install this, and its rackup
dependency on the command line:
gem install rackup
gem install sinatra
Now, open server.rb
and add a route.
require 'sinatra'
get '/' do
'Hello, pizza lovers!'
end
You should be able to run ruby server.rb
and visit localhost:4567
in your browser. Neat!
Using very little code, we’ve created a server that can respond to HTTP requests. There are a few things happening behind the scenes to make this work.
Sinatra acts as a layer between us (the developers) and the Rack middleware. Using the Sinatra DSL syntax, we can tell our application “hey application, please respond to HTTP GET requests to the ‘/’ path with whatever I put in this block of code.” Imagine a world where you could nest a controller action underneath a route in the Rails routes.rb
file– this is kind of what we’re doing here.
Now that we have a basic understanding of how to write an endpoint/route in Sinatra, we should make this file more useful as a microservice.
require 'sinatra'
get '/' do
'Hello, pizza lovers!'
end
get '/suggestion' do
toppings = ['banana peppers', 'mushrooms', 'pineapple', 'crushed garlic']
"Want something different on your pizza today? Try #{toppings.sample}!"
end
This is fine if all we want from the /suggestion
URI is a plain text list of a few toppings. For anything more complex, we’ll want to render a view.
mkdir views
touch views/suggestion.erb
Note: in Sinatra, the file extension for views should be .erb
instead of .html.erb
.
Just like in Rails, we can pass data to the view using an instance variable. However, unlike rails, Sinatra has no concept of a filetree convention with which to infer which view to render; we’ll have to tell it explicitly by passing a symbol to the erb
keyword at the bottom of the route.
# server.rb
get '/suggestion' do
@toppings = ['banana peppers', 'mushrooms', 'pineapple', 'crushed garlic']
erb :suggestion
end
Now add this to the view:
# views/suggestion.erb
<h1>Try something new!</h1>
<p>Today's suggestion is: <%= @toppings.sample %>
Visit localhost:4567/suggestion
to verify this is working.
Finally, let’s abstract our list of toppings to its own file. That way, it isn’t the responsiblity of server.rb
to maintain this; instead we’ll add toppings to a file that gets required and referenced in certain routes.
touch topping_list.rb
In this file, we’ll use a constant for the toppings we want to randomize in suggestions. Because Sinatra wont autoload this, remember to require './topping_list
in your server file.
class ToppingList
CURRENT = [
'banana peppers',
'mushrooms',
'pineapple',
'crushed garlic',
'artichokes',
'olives'
]
def self.suggestion
CURRENT.sample
end
end
Now we can use this encapsulated list by calling the new class method in our route for better SRP:
# server.rb
get '/suggestion' do
@suggestion = ToppingList.suggestion
erb :suggestion
end
Error handling
Error handlers run within the same context as routes. To handle a missing route or resource matching a request, we can render some output in a not_found
block.
not_found do
"You requested a route that doesn't exist."
end
Visiting localhost:4567/some_incorrect_route
should display the text.
But what about unhandled exceptions leading to 500 Internal Server Errors? Let’s replicate this issue and handle the exceptions in a different type of block.
configure do
set :show_exceptions, false
end
get '/force_an_error' do
"hello".gsub
end
error do
'Sorry, there was an error:' + env['sinatra.error'].message
end
Don’t skip the ‘configure’ step! In the development environment, Sinatra will show a stack trace in the browser by default. We want to handle exceptions gracefully instead, which is why we set this configuration to false
.
Try visiting localhost:4567/force_an_error
in your browser, and experiment with raising different exceptions in that route. They should all be caught by the error
block we added, and the correct error message should always be displayed.
Testing
In the future, we highly recommend a TDD approach. For now, let’s at least add some tests for everything we built today.
gem install rack-test
touch server_test.rb
In this new test file, set an environment variable to indicate a test environment. Then, require server.rb
, rspec
, and rack/test
.
ENV['APP_ENV'] = 'test'
require 'server'
require 'rspec'
require 'rack/test'
Next we’ll set up an RSpec.describe
block. Make sure to include Rack::Test::Methods
.
# server_test.rb
ENV['APP_ENV'] = 'test'
require './server'
require 'rspec'
require 'rack/test'
RSpec.describe 'Pizza Topping Suggestions App' do
include Rack::Test::Methods
def app
Sinatra::Application
end
describe '/' do
it 'displays a greeting' do
get '/'
expect(last_response).to be_successful
expect(last_response.body).to eq('Hello, pizza lovers!')
end
end
end
Before you go any further in this lesson plan, try to write tests for the /suggestion
route on your own.
Did you give it your best effort? Great, here’s the finished test file:
# server_test.rb
ENV['APP_ENV'] = 'test'
require './server'
require 'rspec'
require 'rack/test'
RSpec.describe 'Pizza Topping Suggestions App' do
include Rack::Test::Methods
def app
Sinatra::Application
end
describe '/' do
it 'displays a greeting' do
get '/'
expect(last_response).to be_successful
expect(last_response.body).to eq('Hello, pizza lovers!')
end
end
describe '/suggestion' do
it 'suggests a pizza topping' do
get '/suggestion'
expect(last_response).to be_successful
expect(last_response.body).to include('Try something new!')
end
end
describe 'non-existent route' do
it 'returns an error message' do
get '/bad_route'
expect(last_response).to_not be_successful
expect(last_response.body).to include("You requested a route that doesn't exist.")
end
end
end
Don’t forget to write unit tests for topping_list.rb
! Those tests won’t interact with Sinatra or Rack, so if you need a refresher you can review this TDD lesson.
Sinatra for microservices
Discuss: why are you considering Sinatra for your microservices in your project?
As you’ve seen, Sinatra is a powerful but simple tool for quickly putting together web applications in Ruby. Because it’s so flexible, there are many ways you could choose to utilize it in your application architechture, all of which have their own advantages and disadvantages.
In our Pizza Shop example today, we built a microservice which would return html views with pizza topping suggestions. We did not connect to a database or set up routes to create/update/delete resources, but we certainly could. Take 5-10 minutes to brainstorm other ways we could abstract Pizza Shop App functionality into a Sinatra microservice (consider diagramming these ideas with a peer).