To begin, letâs take a moment to address a specific question: âwhat exactly is Sinatra?â Weâll start with a somewhat broad, sweeping answer, and spend the remainder of our time together drilling down into the finer details.
Sinatra is a domain-specific language for building websites, web services, and web applications in Ruby. It emphasizes a minimalistic approach to development, offering only what is essential to handle HTTP requests and deliver responses to clients.
Note
At a high-level, a domain-specific language is one that is dedicated to solving a particular type of problem. For example, SQL (structured query language) is designed to facilitate interaction with relational database systems. By contrast, a general-purpose language such as Ruby can be used to write code in many different domains.
This is a somewhat simplified view of things; if youâre interested in delving deeper into the landscape of DSLs consider Martin Fowlerâs excellent âDomain-Specific Languagesâ (Addison-Wesley).
Written in less than 2,000 lines of Ruby, Sinatraâs syntax (while expressive) is simple and straightforward. If youâre looking to rapidly assemble an API, build a site with minimal fuss and setup, or create a Ruby-based web service, then Sinatra has quite a bit to offer. As weâll see later, Sinatra applications can also be embedded into other Ruby web applications, packaged as a gem for wider distribution, and so on.
In this chapter, our goal is to get off the ground as quickly as possible. Weâll install Sinatra and a supporting web server to host our application, then create a simple app that responds to an HTTP request.
Note
A quick disclaimer: at the outset, the code we present will not have much in the way of robust error handling. Instead we will focus purely on the syntax required to express core concepts without distraction. In discussing the fundamentals (particularly in Chapter 2), we will establish the built-in mechanisms that Sinatra provides for handling faults of varying nature.
Of course, normal Ruby best practices hold true; Avdi Grimm offers excellent coverage on the nuances of handling Ruby error conditions at http://exceptionalruby.com/.
Weâll get into Sinatraâs features and syntax in a moment; at the outset, it would be useful to define some parameters around what makes Sinatra distinctive and unique in the Ruby web ecosystem.
Sinatra is not a framework; youâll find no built-in ORM (object-relational mapper) tools, no pre-fab configuration files...you wonât even get a project folder unless you create one yourself.
While it may not seem like it now, this can be quite liberating. Sinatra applications are very flexible by nature, typically no larger than they need to be and can easily be distributed as gems.
A notable example along these lines is Resque, a very handy job processor created by the folks at GitHub. If you happen to install it, youâll find it comes with a Sinatra application that can be used to monitor the status of the jobs you create.
Sinatra does not force you to adhere to the model-view-controller pattern, or any other pattern for that matter. It is a lightweight wrapper around Rack middleware and encourages a close relationship between service endpoints and the HTTP verbs, making it particularly ideal for web services and APIs (application programming interfaces).
Note
Model-view-controller (specifically the Model2 variant common to the web) is a way of architecting applications that many web frameworks have adopted. Although these frameworks may have routing rules that are similar in some ways to Sinatraâs routes, they typically also enforce them strictly with requirements on folder names and project hierarchies.
The Padrino Framework, available from http://www.padrinorb.com/, brings the Sinatra core into the MVC world. If youâre a Rails developer and find youâre missing some of the features it provides, you might want to give Padrino a try.
GitHub, Heroku, BBC, thoughtbot, Songbird, Engine Yard, and many others are active users of Sinatra in production environments. You can rest assured that by learning and implementing Sinatra you are working with a tested and proven solution that supports a scalable, responsive web experience.
Note
Initially developed by Blake Mizerany, the continued development and support of Sinatra is provided by the team at Heroku.
Believe it or not, itâs not uncommon to find entire Sinatra applications encapsulated in a single physical file. You can certainly build larger applications, and weâll cover some helpful ways to lay out applications throughout the course of the book.
There are two primary approaches to building Sinatra applications: classic and modular. Theyâre similar, but with a few caveats: you cannot have multiple classic applications running in one Ruby process, and classic mode will add some methods to Object (which could be a problem if you want to ship your application as a gem). You can also create Sinatra apps on the fly, entirely in code, from within another application.
Whatâs the difference between the two? Well, the quick answer is that in modular mode, you explicitly subclass Sinatra and build your application within that scope; in classic mode, you just require Sinatra and start defining endpoints. Both have their pros and cons, depending on your needs.
Weâre going to explore the classic style of Sinatra application first in this book, then dive a little deeper into Rack, modular applications, and so on. Youâll have a good sense of both methods shortly.
All these benefits sound great, but it doesnât indicate that Sinatra is the correct choice for every web-facing application under the sun. If youâre looking to build the next gigantic social network, you certainly could do it in Sinatra, but it would require considerably more wiring on your part compared to the conveniences provided by a framework (such as Rails or Padrino). The choice of tools becomes one of balance, and youâll need to make judgment calls based on the needs of the project.
Later in the book we will demonstrate ways to better organize projects as they grow in scope. Any application can get away from you if you let it, and Sinatra applications are no exception.
Beyond the close relationship it has with the underlying web protocols, Sinatra has also inspired a number of tools in languages such as Microsoft .NET (Nancy), Perl (Dancer), Lua (Mercury), and quite a few more.
Investing time in learning Sinatra is not only beneficial by way of becoming better acclimated with the tools and protocols that power the web, but can also serve as a convenient springboard to grokking other languages.
Installing Sinatra is straightforward; from the command line, simply
type gem install sinatra
. At the
time of this writing, the current version of Sinatra is 1.3.1.
Warning
Earlier versions of Sinatra had some issues with the Ruby 1.9.x family. Since 1.2, Sinatra plays nicely with Ruby 1.9.2, but you should be aware of the potential for issues with older combinations.
The installation is brief and fairly unceremonious. When itâs
finished, we recommend you also install the Thin web server by typing
gem install thin
. Sinatra will
automatically use Thin to handle communication with clients, if it is
available.
Why Thin as opposed to other server options? Most Ruby web developers are familiar with WEBrick, a web server written entirely in Ruby. Zed Shaw later introduced Mongrel, which gained popularity as a faster and more stable platform for Ruby web applications. Thin continues this evolution by using code from Mongrel to parse HTTP requests but improves network I/O performance via EventMachine, which manages evented network communication. If Thin is not installed, Sinatra will first try to run with Mongrel, choosing WEBrick if Mongrel isnât available either.
Note
Sinatra 1.3.1 adds a number of new features, notably support for streaming. At the moment, there is a known issue with WEBrick and streaming: the response from the server arrives at the client all at once. This is currently being addressed.
Warning
For Windows users: depending on the specifics of your environment, you may need to build Thin from source. To do so, youâll need to install a C compiler or opt for one of the Ruby development kit versions. You can certainly run Sinatra without Thin, but be aware that Thin is known to perform better under high load than both Mongrel and WEBrick.
Itâs painless to get a Sinatra application off the ground. Open the text editor of your choice and enter the code in Example 1-1. The syntax is readable, but weâll discuss the finer details in a moment.
Save this file as server.rb.
Once youâve done so, type ruby
server.rb
at the command prompt. You should be notified that
Sinatra has taken the stage, as shown in Figure 1-1.
Note
If you happen to still use Ruby 1.8 and you run into âno such file
to loadâ exceptions, try ruby -rubygems
server.rb
instead. To avoid those extra characters, you can
simply set the environment variable RUBYOPT
to -rubygems
. On Linux or Mac OS X, this can
easily be done by adding RUBYOPT=-rubygems
to your .bashrc in your home directory.
Note
By default, the application will listen on port 4567. You can
select any available port by typing ruby
server.rb -p
.port_num
Open a web browser and navigate to http://localhost:4567/. Your Sinatra application should respond with the cheerful greeting displayed in Figure 1-2.
Weâve installed Sinatra and Thin, created a simple application, and verified that it responds properly to an HTTP GET request. So whatâs happening under the hood?
Sinatra is essentially a lightweight layer separating you as a developer from a piece of Ruby middleware called Rack. Rack wraps HTTP requests to help standardize communication between Ruby web applications and web servers. Sinatra abstracts Rack, allowing you to focus solely on responding to HTTP requests without worrying about the underlying plumbing.
The only aspect that should look foreign to the average Ruby developer is line 3:
get '/' do
Here we get our taste of the Sinatra DSL syntax, which is typically expressed in the form verb ârouteâ do. In our code, we are instructing the application to respond to HTTP GET requests to the path '/'; our response is composed by the block we provided for behavior. This composite endpoint is referred to as a route. Sinatra applications respond to one or more routes to provide their functionality.
This is part of the Sinatra magic; this code doesnât look like a typical method definition because in actuality, itâs not. Itâs actually a method call.
Sinatraâs base class defines a handful of public methods matching the HTTP verbs (which weâll discuss in depth in Chapter 2). The methods accept paths, options, and blocks. The block in Example 1-1 is the implicit return of âHello, world!â and this is what gets evaluated deeper in the library. By making use of Rubyâs flexible nature with regard to brackets and parentheses, Sinatra is able to provide a syntax that reads quite naturally.
Note
Itâs definitely worth taking a tour of the Sinatra source code at https://github.com/sinatra/sinatra when time permits.
Warning
Routes in your application are matched in top-down order; the first route that matches the incoming request is the one that gets used. This becomes an important point when we begin creating routes that include wildcards or other optional parameters where very different actions can occur depending on the values provided in the request. Weâll revisit this point with concrete examples in Chapter 2.
This route is certainly on the simpler side of things; indeed the point is to demonstrate how little code it takes to create a âcompleteâ application. More complex routes can respond to various HTTP verbs, contain wildcards, different types of pattern matches, and multiple routes can respond with the same action. Weâll greatly expand on routes in Chapter 2.
One critical key point when developing with Sinatra is that the program doesnât respond to anything you donât tell it to. We can see this quite clearly with a quick Telnet session, demonstrated in Example 1-2.
From the command line, type telnet
0.0.0.0 4567
to establish a session with your application.
Type the lines that are not italicized in the example below exactly as
they appear. After the Host: 0.0.0.0
line, press return a second time. This ends the âheadersâ
section, which weâll talk more about in the next chapter. For now itâs
sufficient to say that this tells the server you donât have anything
further to say and it should start processing.
Note
The lines that are italicized and indented are the responses from the web server. If you encounter any errors (such as the connection being closed) start Telnet again and ensure that each line is typed accurately.
After connecting to the Sinatra application, we issued a GET request to the â/â route. The application promptly responded with our chipper greeting (and a tongue-in-cheek header identifying the codename for the latest version of Thin).
Note
Thin releases typically have codenames like in the example above. For example, version 1.2.2 is named âI Find Your Lack of Sauce Disturbing.â
What would happen if we were to issue a POST to the application? Letâs give it a try; weâll POST the payload âfoo=barâ to the â/â route.
Note
The same rules apply; the non-italicized lines should be typed
exactly as shown, there should be a blank line between Content-Length: 7
and foo=bar
, and there should be a blank line
after foo=bar
.
POST / HTTP/1.1 Host: 0.0.0.0 Content-Length: 7 foo=bar HTTP/1.1 404 Not Found X-Cascade: pass Content-Type: text/html;charset=utf-8 Content-Length: 410 Connection: keep-alive Server: thin 1.2.10 codename I'm dumb <!DOCTYPE html> <html> <head> <style type="text/css"> body { text-align:center;font-family:helvetica,arial;font-size:22px; color:#888;margin:20px} #c {margin:0 auto;width:500px;text-align:left} </style> </head> <body> <h2>Sinatra doesn't know this ditty.</h2> <img src='/__sinatra__/404.png'> <div id="c"> Try this: <pre>post '/' do "Hello World" end</pre> </div> </body> </html>
Yikes. This, however, is to be expected. Sinatraâs âstay out of the wayâ approach carries with it the understanding that by staying out of the way, you are expected to pick up the slack. Weâll learn how to do so in Chapter 2.
If you examine the response from the server, youâll notice itâs an HTML page. Sinatra is pretty helpful; it noticed you were trying to issue a request with a verb it didnât recognize, so itâs giving you a hint. More than a hint, really...itâs flat-out telling you how you can make the error disappear. The visual form of this message is shown in Figure 1-3.
You donât have to address this now; itâs just helpful to know that Sinatra typically chimes in with particularly useful information when routing is askew.
Web development can be so serious; letâs take a moment to have a little fun and make a Sinatra application that will play rock, paper, scissors with us.
Note
Weâll touch briefly on whatâs happening at each stage of the process, but donât worry too much about the particulars right now; the goal is just to whip up a quick little app. Weâll cover all the concepts used shortly.
To begin, create a new file called game.rb
in a folder of your choosing. Weâll get
the application rolling by defining a route; players will make requests to
this route and provide the throw theyâd like to make. Example 1-3 shows the starting point of our game.
Example 1-3. Starting the rock, paper, scissors application
require 'sinatra' get '/throw/:type' do # play here end
Now we should define the moves that are valid. Weâll also specify that weâre only returning plain old text (as opposed to HTML) when the player makes a move. The code to handle this is shown in Example 1-4.
Example 1-4. Specifying things that should happen prior to handling the request
require 'sinatra' # before we process a route, we'll set the response as # plain text and set up an array of viable moves that # a player (and the computer) can perform before do content_type :txt @defeat = {rock: :scissors, paper: :rock, scissors: :paper} @throws = @defeat.keys end get '/throw/:type' do # play here end
Great, now we have a set of valid moves defined and our route will
return plain text. Next, we should handle the input from the user and make
sure itâs valid by checking against @throws
(see Example 1-5).
Example 1-5. Validating the user input
require 'sinatra' # before we process a route, we'll set the response as # plain text and set up an array of viable moves that # a player (and the computer) can perform before do content_type :txt @defeat = {rock: :scissors, paper: :rock, scissors: :paper} @throws = @defeat.keys end get '/throw/:type' do # the params[] hash stores querystring and form data. player_throw = params[:type].to_sym # in the case of a player providing a throw that is not valid, # we halt with a status code of 403 (Forbidden) and let them # know they need to make a valid throw to play. if !@throws.include?(player_throw) halt 403, "You must throw one of the following: #{@throws}" end end
Now if a user tries to throw foobar
, the application will inform her itâs an
invalid move and provide suitable options in return; processing will stop
immediately when halt
is called.
Next, letâs get the computer to pick a random move and compare it to the user move. Weâll provide some appropriate messages for each case (win, lose, and tie), as shown in Example 1-6.
Example 1-6. The final rock, paper, scissors application
require 'sinatra' # before we process a route, we'll set the response as # plain text and set up an array of viable moves that # a player (and the computer) can perform before do content_type :txt @defeat = {rock: :scissors, paper: :rock, scissors: :paper} @throws = @defeat.keys end get '/throw/:type' do # the params[] hash stores querystring and form data. player_throw = params[:type].to_sym # in the case of a player providing a throw that is not valid, # we halt with a status code of 403 (Forbidden) and let them # know they need to make a valid throw to play. if !@throws.include?(player_throw) halt 403, "You must throw one of the following: #{@throws}" end # now we can select a random throw for the computer computer_throw = @throws.sample # compare the player and computer throws to determine a winner if player_throw == computer_throw "You tied with the computer. Try again!" elsif computer_throw == @defeat[player_throw] "Nicely done; #{player_throw} beats #{computer_throw}!" else "Ouch; #{computer_throw} beats #{player_throw}. Better luck next time!" end end
And thatâs it! The game will let the player know if thereâs a tie, and if not it will compare the player and computer throws to determine whether the player won or lost.
Type ruby game.rb
to start
the game, then browse to http://localhost:4567/throw/scissors to try your luck
against the machine. Figure 1-4 shows how well we
fared.
It should be obvious at this point that Sinatra has been designed with rapid development in mind. That said, thereâs a lot more to explore. So far, weâve discussed what Sinatra is and what makes it distinctive from other Ruby web development tools. Next, we installed Sinatra as well as Thin, a web server to host our code locally. We also created a simple application that responds to a single route and saw how Sinatra handles missing routes. Finally, we created a game of rock, paper, scissors that pits human versus machine in a battle to the death.
Moving ahead, weâll take an in-depth look at Sinatra routing, how it maps to the HTTP specification, and discuss how to create more comprehensive applications.
Get Sinatra: Up and Running now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.