Introduction
Sinatra (http://sinatrarb.com/) is a minimalist web framework. If you are familiar with express.js, it was inspired by Sinatra. I have found it to be incredibly useful and fast to work with. It is known for being very simple and easy to use. This tutorial will cover some of the common tasks that I have used.
As always, refer to the official documentation for complete information and to learn more.
Installation
To install Sinatra, just use gem
to install the package.
gem install sinatra
Some Linux distributions have a package already created for Sinatra. You might be able to install using your Linux package manager. For example:
# Ubuntu
sudo apt install ruby-sinatra
# Fedora
sudo dnf install rubygem-sinatra
Get Sinatra documentation
There are a couple options for viewing the Sinatra documentation. Here are instructions on getting the online and offline documentation.
Online documentation
You can view the official documentation online at http://sinatrarb.com/documentation.html.
Local gem documentation
You can view documentation locally by using the gem server.
gem server
Then visit the gem server in your browser. It defaults to http://localhost:8808/.
You will find a link to the Sinatra web page and the local rdocs. The local URL will be something similar to http://localhost:8808/doc_root/sinatra-2.0.5/.
Local system documentation
If you installed Sinatra using a Linux package manager like apt
or dnf
, it might have come with the documentation. Use your
package manager to search
# In Ubuntu
apt-cache search sinatra
sudo apt install ruby-sinatra
dpkg -L ruby-sinatra
cd /usr/share/doc/ruby-sinatra
ls
sudo gunzip README.md.gz
less README.md
In Fedora, a separate package contains the documentation.
After installing the rubygem-sinatra-doc
package, the README.md
file will be available in /usr/share/gems/gems/sinatra-*
.
dnf search sinatra
sudo dnf install rubygem-sinatra
sudo dnf install rubygem-sinatra-doc
rpm -ql rubygem-sinatra-doc
less /usr/share/gems/gems/sinatra-2.0.3/REAMDE.md
Hello World
Here is an example that demonstrates just how simple Sinatra is to start using.
# hello.rb
require 'sinatra'
get '/' do
'Hello, world!'
end
Then run the file and visit the page in the browser or use curl
or wget
to perform a test request.
When starting the file it should announce what port is listening
on in the console, which defaults to 4567.
ruby hello.rb
curl http://localhost:4567
Change default bind address and port
To modify the default listen address and port, can use the set
function.
By default, running the file will listen on localhost:4567.
This is good for security as it only listens locally, but if you want
to make it available publicly or on a different interface, you will
want to override the listen address. Here is an example:
require 'sinatra'
set :bind, '0.0.0.0'
set :port, 9999
get '/' do
"Now listening on all interfaces on port 9999"
end
For a production quality deployment, check out my tutorial: Deploy Ruby Rack Web Apps with uWSGI and Nginx.
Receive data
There are a number of ways to get data from a user, including:
- Inspecting the HTTP request headers
- Receiving a value as part of the URL
- Receiving a value as a URL query parameter
- Receiving a value from a POST form submission
We will look at each of these methods.
Inspect HTTP request headers
The request headers are stored as a Hash in the request environment variable, request.env
.
There are few special things to know about the request.env
Hash though.
- It contains all HTTP headers provided.
- It also contains additional data like
rack.url_scheme
,rack.version
, andrack.multithread
. - Any HTTP headers provided will have the key name transformed:
- Converted to all uppercase
- Hyphens converted to underscores
- Include a prefix of "HTTP_"
- Examples:
- "My-Header: my data" would come through as
request.env['HTTP_MY_HEADER']
- User agent comes through as
request.env['HTTP_USER_AGENT']
- Host comes through as
request.env['HTTP_HOST']
require 'sinatra'
require 'pp' # Pretty print
get '/' do
puts 'Request headers:'
puts request.env.class
pp request.env
'User agent: ' + request.env['HTTP_USER_AGENT']
end
You can test it out with curl http://localhost:4567/ -H "My-header: my data"
Routing with variables
Sinatra allows some complex URL path matching if necessary. We're going to look only at a simple example.
Some more advanced options include using regular exprssions to match complex patterns or matching conditions like a user-agent string. Those methods are out of scope for this tutorial and can be found documented in the official Sinatra documentation.
Here are a couple examples of taking values in the URL.
The first option shows how to take a required parameter
and access it via params
object. The second one shows
how to take an optional parameter and access the variable
using the block declaration style.
require 'sinatra'
# :x is required for matching
# Try /x/testing
# Will not match /x because no trailing slash provided
# Will not match /x/ because :x param is missing
get '/x/:x' do
"x is #{params['x']}"
end
# :y is optional with the ?
# Try /y/testing
# Will not match /y because trailing slash is required
# Will match /y/ since :y is optional
get '/y/:y?' do |y|
"y is #{y}"
end
One thing to keep in mind with routing is that trailing slashes do matter. A route with a trailing slash is different than one without. The URLs must match exactly.
You can make the trailing slash optional with something like /path/?
.
Consider this possibility:
# Both the last slash and the :y parameter are optional
# Will match /y/testing
# Will match /y/ since :y is optional
# Will match /y since the last slash is optional
# Will match /yes since the last slash is optional
# Will not match /yes/ since there is no / after :y
# Will not match /yes/testing since routing is split by slashes
get '/y/?:y?' do |y|
"y is #{y}"
end
The one interesting case with that last example is that
it will match paths that starts with a "y", like /yes
.
The last slash is optional so it then treats the rest of the path
after that as the :y
parameter. However, adding any other slashes
after the :y
parameter though will create a different route that
does not match. For example /yes
matches but /yes/
will not.
Query parameters
Query parameters are also stored in the params
object.
For example, if you had the path http://localhost:4567/myurl?i=123
,
then the i
query parameter would be accessible with params[:i]
.
Post data
Post parameters are also stored in to the same params
variable.
For example, if you had an HTML form with a text input like
<input name="username" type="text" />
, it would be accessible with params[:username]
when submitted via POST.
Return data
Returning data to the requester can be done in several ways including:
- Serving static files
- Modifying or adding HTTP response headers
- Changing response status code
- Returning JSON
- Returning an HTML template
- Returning a specific file
Serve static files
By default, Sinatra serves static files from a public/
directory,
if it exists. Simply put static files in to the public/
directory
and they can be accessed directly. For example, if you had a file,
public/test.txt
then you could access it by visiting
http://localhost:4567/test.txt
.
You can override the location of the public directory by setting
the :public_folder
value like this:
require 'sinatra'
# Use the 'static/' directory relative to
# the location of the Ruby file
PUBLIC_DIR = File.join(File.dirname(__FILE__), 'static')
set :public_folder, PUBLIC_DIR
Modify HTTP response
You can return additional information to the reqeuster by modifying or adding HTTP header or changing the status code.
Modify HTTP response headers
To inspect the response headers, look at the headers
object. To set
the headers, call headers
and pass in the values. It will set
a header if it does not exist and override the header data if it does not
exist.
require 'sinatra'
get '/' do
puts headers
headers "My-Header" => "My Data"
puts headers
headers "My-Header" => "Overriden.", "Extra-Header" => "Data..."
puts headers
'Done'
end
Modify status code
You can access the current status code in the status
object.
To modify the status code, call the status()
function and give it
the status code you want to return. For example, to specify an arbitrary
status code of 999:
require 'sinatra'
get '/' do
status 999
puts status
'Done'
end
Set mime type
Mime types, also known as content types, help the client or web browser
understand what kind of data they are receiving in order to handle it properly.
For example, if it is returning text/html
it might handle it differently
than receiving application/json
text. You can set the content type to
whatever you need and you can also create custom mime types for re-use
and set a default mime type for all requests.
Create custom mime types
require 'sinatra'
configure do
mime_type :my_mime_type, 'text/custom'
end
get '/' do
content_type :my_mime_type
# Or you can directly set a mime type like:
# content_type 'text/mycustomtype'
'Done'
end
Set default mime type
If you want to set application/json
or some other mime type as the default
for all requests, you can do that with the before
hook.
require 'sinatra'
before do
content_type 'application/custom'
end
# All responses will default to `application/custom` type
get '/' do
'Done'
end
Return JSON
Sinatra has a built in mime type for JSON :json
which translates to application/json
.
You could return JSON data by setting the mime type either explicitly in each request
or by setting the default mime type for respones with configure
as
demonstrated in the previous section.
Manually return JSON
Manually by setting importing the Ruby JSON gem with require 'json'
,
setting the response content-type to application/json
and then
calling .to_json()
on your return object to convert it to JSON text.
require 'sinatra'
require 'json'
get '/' do
content_type :json
data = {"Hi" => "Hello", "Bye" => "Goodbye"}
data.to_json
end
Or if you are building an API and you want it to use application/json
by
default:
require 'sinatra'
require 'json'
before do
content_type :json
end
get '/' do
data = {"Hi" => "Hello", "Bye" => "Goodbye"}
data.to_json
end
If you wanted to take it one step further you could create a json
function
that handles both setting the content type and converting the object to JSON text.
require 'sinatra'
require 'json'
def json data_object
content_type :json
data_object.to_json
end
get '/' do
json {"Hi" => "Hello", "Bye" => "Goodbye"}
end
Use sinatra-contrib's sinatra/json gem
There is a gem named sinatra-contrib
that contains some extras like
a convenience method for JSON. You have to install another dependency
for this to work. The previous examples of returning JSON do not
require any additional dependencies. You will need to install
sinatra-contrib:
gem install sinatra-contrib
Then in your code you import sinatra/json
and use their json
function.
require 'sinatra'
require 'sinatra/json' # gem install sinatra-contrib
get '/' do
json 'Hi' => 'Hello', :SomeKey => 'SomeValue'
end
Return a file
Read the previous sections about creating custom mime types (content types), setting mime types setting default mime types. mime types.
You can use send_file
to return static files.
The send_file
function will automatically determine the mime type and content length.
You can specify them if necessary and also set the last-modified time and
status if necessary as parameters to the send_file
function.
require 'sinatra'
get '/' do
# Returns `test.txt` from the current directory
send_file 'test.txt'
end
If you want the client's web browser to treat the file as a download as opposed
to something that should be displayed in the browser, you want to set the
Content-Disposition
header to attachment
. You can do this by either calling
the attachment
function somewhere in your handler, or pass :disposition => 'attachment'
to the send_file
function.
require 'sinatra'
get '/' do
attachment
send_file 'test.txt'
end
Or:
require 'sinatra'
get '/' do
send_file 'test.txt', :disposition => 'attachment'
end
You can also set the name of the downloaded file separately if desired:
require 'sinatra'
get '/' do
send_file 'test.txt', :disposition => 'attachment', :filename => 'custom.txt'
end
Templates
Sinatra supports templates out of the box. It can handle HAML, ERB, and Markdown templates among dozens others. Personally I prefer ERB as it is basically regular HTML with the ability to drop in Ruby code, very similar to PHP files. Using ERB templates also does not require any additional dependencies.
Sinatra assumes a default template directory named views/
.
Layout templates
Sinatra will look for a default layout of layout.erb
and checks
for its existence. If a layout exists it will default to that.
If a layout.erb
does not exist, and a layout is not explicitly
passed to the erb
function, then no layout will be used to encapsulate
the final template.
views/layout.erb
example:
<%# Optional default layout file `views/layout.erb` %>
<html>
<body>
<%= yield %>
</body>
</html>
All content from the sub-template will end up in the yield
block.
Return a template
You can return a template using the following syntax.
This will look for templates views/index.erb
.
require 'sinatra'
get '/' do
# Looks for `views/index.erb`
erb :index
# Templates in subdirectories can be reached like:
# erb :'account/index'
end
The views/index.erb
template can contain HTML like this:
<h1>Welcome</h1>
<p>This is my index template.</p>
Pass variables to templates
This example shows how to pass variables to a template.
require 'sinatra'
get '/hello' do
erb :hello, :locals => {:greeting => "Howdy"}
end
In views/hello.erb
you can use <%= =>
tags to echo a variable:
<p>
<%= greeting %>
</p>
You can also use regular Ruby statements inside <% %>
blocks.
For example if an iterable object was passed to the ERB template
you could do:
<% my_iterable.each do |item| %>
<p>
<%= item %>
</p>
<% end %>
One important thing to keep in mind is that any data provided
by a user could be malicious. You should always escape any HTML
provided to lower cross-site scripting and cross-site request forgery
risks. You can use the Rack::Utils.escape_html()
function. It will
be available after calling require 'sinatra'
.
For more tips on HTML escaping including how to set up a convenient helper function
and turn on auto-escape with Erubis,
check out the official Sinatra FAQ.
Session data
Sessions are a way of persisting data for a user that can be accessed across multiple requests. A common use for sessions is to store a token used to validate authentication on the server.
To use them in Sinatra you simply have to enable them and then the session
object becomes available. It automatically handles the overhead of managing
sessions. It uses Rack under the hood to manage sessions and cookies, which
you can utilize directly if desired. It uses serialized, signed and encrypted cookie
data rather than a database to maintain the session information. Users cannot
read the data since it is encrypted, and they cannot tamper with it because it is signed.
require 'sinatra'
enable :sessions
get '/set' do
session[:mydata] = 'My data'
'Complete'
end
get '/get' do
"Data: #{session[:mydata]}"
end
Redirects
Redirecting is as simple as calling the redirect()
function.
Another useful function provided is the to()
function. Using to()
will generate a fully qualified URL based on the current server and protocol.
For example, to('/')
in the default setup would return the string http://localhost:4567/
.
By default it uses a 302 temporary redirect code.
require 'sinatra'
get '/' do
'Welcome back.'
end
get '/goaway' do
# Optionally, use the to() function to
# generate a fully qualified URL (http://localhost:4567/)
# redirect to('/')
# Or simply go back to previous page
# redirect back
# To specify the return code
# redirect '/', 301 # Redirect with 301 permanent
redirect '/'
end
Authentication
Rack provides a way to protect your application with basic HTTP authentication.
You perform your authentication check and then return true
or false
depending on whether you want to grant access to the username and password
combination.
require 'sinatra'
use Rack::Auth::Basic, "Authorization Required" do |user, pass|
# Perform some kind of check and return true/false
access_permitted = false
if user == "admin" and pass == "admin"
access_permitted = true
end
access_permitted
end
get '/' do
"Access granted"
end
Logging
Sinatra provides a logger
object that is available in each request.
By default it outputs info, warn, and error messages, but not debug.
Consult the official documentation
for instructions on configuring log levels. Using different log levels
allows you to tune the verbosity of logging and makes certain events
more easily greppable.
require 'sinatra'
get '/' do
logger.info 'Informational log'
logger.warn 'Warning log'
logger.error 'Error!'
logger.debug 'Debug message'
end
Deploy Sinatra to production
I have written a dedicated tutorial to deploying Ruby Rack applications using nginx and uWSGI:
Deploy Ruby Rack Web Apps with uWSGI and Nginx.
Conclusion
After reading this you should have a solid grasp of Sinatra and its fundamentals to make web applications. You sould know how to install and get started as well as where to go to learn more about the advanced features it provides.