- Understand how Sinatra simplifies developing web applications
- Receive a request in Sinatra and send different kinds of responses
- Create dynamic routes in Sinatra
A web application framework (WAF) is a software framework that is designed to support the development of dynamic websites, web applications, web services and web resources. The framework aims to alleviate the overhead associated with common activities performed in web development. — Wikipedia
Building dynamic web applications in any language is a complex job requiring intimate knowledge of hundreds of technologies and specifications. The good news, however, is that many of these requirements are universal and every web application must conform to these standards.
For example, any robust web application will need to handle request routing
and provide a mechanism for the application to respond to different URLs with
the appropriate response. For example, a blog application may handle a
request to GET /posts
to show all the recent blog posts, and a request to
GET /authors
to list all the authors.
Similarly, web applications require the ability to render templates to produce
consistently structured dynamic content. A request to GET /posts/1
must render
the HTML for the first post, just as a request to GET /posts/2
will render
identically structured HTML (but with different content) for the second post.
This is possible because of templates.
Web frameworks should also provide a way to send data back in a variety of different formats. For example, we should be able to produce full HTML pages, but we should also be able to produce simple JSON strings to represent the data in our applications.
Web frameworks take all these routine and common requirements of any web application and abstract them into code and patterns that provide these functionalities to your application without requiring you to build them yourself.
Frameworks provide structure and libraries that allow you to focus on your application and not applications in general. The bigger the framework, the more you can rely on it to provide you with common needs. The smaller the framework, the more you'll have to build things yourself.
Sinatra is a small web framework that provides a Domain Specific Language (or DSL) implemented in Ruby. It was created by Blake Mizerany and provides a lightweight option for developing simple web applications. Sinatra is Rack-based, which means it uses Rack under the hood and can use many tools designed to work with Rack. It's been used by companies such as Apple, BBC, GitHub, LinkedIn, and more.
Essentially, Sinatra is nothing more than some pre-written methods that we can include in our applications to turn them into Ruby web applications.
Unlike Ruby on Rails, which is a full-stack web development framework that provides everything needed from front to back, Sinatra is designed to be lightweight and flexible. It provides you with the bare minimum requirements and abstractions for building simple and dynamic Ruby web applications.
In addition to being a great tool for certain projects, Sinatra is a great way to get started in web application development with Ruby and will prepare you for learning other larger frameworks, including Rails.
To see what Sinatra is all about, let's build out a quick demo application.
First, run bundle install
to install the Sinatra gem from the Gemfile. Then,
take a look at the code in the config.ru
file:
require 'sinatra'
class App < Sinatra::Base
get '/hello' do
'<h2>Hello <em>World</em>!</h2>'
end
end
run App
Run the app with:
$ rackup config.ru
Just like with our Rack example, this will run a server locally. Visit
http://localhost:9292/hello in the browser to
make a request to our Sinatra server and see the response. It even takes care of
sending back the 200 status code, and setting the Content-Type
header to
text/html
, which we had to do manually with Rack. Nice!
Let's break down what's happening in this simple example, and then add a few more features to our Sinatra server.
One of the biggest benefits of using Sinatra is that it has a very easy-to-read Domain-Specific Language, or DSL, for writing multiple routes in an application.
In the App
class above, we're inheriting this routing DSL from the
Sinatra::Base
class, which allows us to define routes like this inside the
class:
get '/hello' do
'<h2>Hello <em>World</em>!</h2>'
end
You can quickly see what this code is doing: it's setting up a block of code
that will run whenever a GET
request comes in to the /hello
path of our
application. Whatever is returned by the block will be sent back as the
response: in this case, it's a string representing some HTML.
We can also easily define more than one route:
class App < Sinatra::Base
get '/hello' do
'<h2>Hello <em>World</em>!</h2>'
end
get '/potato' do
"<p>Boil 'em, mash 'em, stick 'em in a stew</p>"
end
end
Note: After making these changes, you'll need to restart the server before you can try them out in the browser. You can stop the server with
control + c
. If you encounter an error when running your server about the port being in use, refer to this StackOverflow post to find and stop a process running on a specific port.
Compared to the conditional logic we needed to write by hand in Rack, we think you'll agree that this DSL provides a much nicer developer experience!
Sinatra also provides friendlier error messages when your application doesn't work as expected. For example, try visiting a route that doesn't exist, like http://localhost:9292/nope. You should see an error screen, along with a suggestion on how to fix this particular error by updating your code. Nice!
Another feature of Sinatra that helps simplify our server-side code is the ability to easily send back a response in different formats. For example, we can have it send back some HTML dynamically by generating a string with Ruby:
class App < Sinatra::Base
get '/dice' do
dice_roll = rand(1..6)
"<h2>You rolled a #{dice_roll}</h2>"
end
end
Every time we make a request, we send back a different number in the HTML string.
But what if instead of HTML, we wanted to have our application generate some JSON data, which we could use with a separate frontend application like React?
Well, that's as easy as using the .to_json
method to convert a Ruby hash or array
to a valid JSON string:
class App < Sinatra::Base
get '/dice' do
dice_roll = rand(1..6)
{ roll: dice_roll }.to_json
end
end
We can also update the default response header for all responses to indicate that our server is returning a JSON-formatted string:
class App < Sinatra::Base
# Add this line to set the Content-Type header for all responses
set :default_content_type, 'application/json'
get '/dice' do
dice_roll = rand(1..6)
{ roll: dice_roll }.to_json
end
end
Now, restart the server and visit our new endpoint at http://localhost:9292/dice. Refresh the page to your heart's content! You'll get some new JSON data each time.
One other powerful feature of Sinatra is the ability to define dynamic routes.
To give a contrived example, let's say, for instance, that we were building an
API for performing math operations. When a request comes in to our server to the
path /add/1/2
, we'd want to add 1 + 2 and send back a response of 3.
We could try setting this up like our other routes:
class App < Sinatra::Base
get '/add/1/2' do
sum = 1 + 2
{ result: sum }.to_json
end
end
This works just fine for 1 and 2, but what if we want to add other numbers, like
/add/2/5
? Well, we could try to define routes for those other numbers
manually, but... nope, that won't work. There are way too many numbers for that
to be practical!
What we can do instead is write out a route using a special syntax with named parameters, which looks like this:
class App < Sinatra::Base
# :num1 and :num2 are named parameters
get '/add/:num1/:num2' do
num1 = params[:num1].to_i
num2 = params[:num2].to_i
sum = num1 + num2
{ result: sum }.to_json
end
end
By defining our route with this special syntax, any requests that match the
pattern /add/:num1/:num2
will result in this route being used. So making a
request to /add/1/2
will use this route, and so will /add/2/5
.
The other benefit of using this syntax is that we get access to additional data from the url in a special variable known as the params hash.
We'll explore the params hash in more detail in future lessons, but you can think of it as a way for us to pass in some additional arguments to a route handler.
For example, a GET /add/1/2
request would result in a params hash that looks
like this:
{"num1"=>"1", "num2"=>"2"}
And a GET /add/2/5
request would result in a params hash that looks
like this:
{"num1"=>"2", "num2"=>"5"}
While this example of setting up an API just to do math is certainly not the
most practical, being able to set up dynamic routes and access data via the
params hash will become incredibly useful once we start working with Active
Record models. For instance, we could set up a route that returns a specific
Game
from the games
table, formatted as JSON, using very similar code to
what we used above:
get '/games/:id' do
game = Game.find(params[:id])
game.to_json
end
This example won't work yet, since we don't have a Game
class set up, but
we'll soon see how to get this code working!
In this lesson, we covered a lot of the core functionality of Sinatra. We saw how to use Sinatra's routing DSL to easily set up a server to handle requests using different HTTP verbs and paths. We also saw how to generate both HTML and JSON responses. Finally, we used dynamic routes to handle requests and access data about a request via the params hash. You're well on your way to creating your own web servers with Sinatra!