In Chapter 3, we saw that normal Sinatra applications
actually live in Sinatra::Application
,
which is a subclass of Sinatra::Base
.
Apparently, if we don’t use the Top Level DSL, it is possible to just
require 'sinatra/base'
. And it shouldn’t
be surprising by now that it is common practice to actually do so. If we do,
we usually don’t use Sinatra::Application
, but instead we create our
own subclass of Sinatra::Base
.
This style is called a modular application, as
opposed to classic applications that are using the Top
Level DSL. While classic applications assume a certain style by default and
extend Object
, starting with a modular
application assumes next to nothing about your application setup.
Caution
For some reason, it is a common misconception that modular applications are superior to classic applications, and that really advanced users only use modular style. From time to time it has even been proposed to drop classic style all together. This is utter nonsense and no one on the Sinatra core team shares this view. Sinatra is all about simplicity and if you can use a classic application, you should.
But why would one want to use modular style? If you activate the Top
Level DSL (by requiring sinatra
), Sinatra
extends the Object class, somewhat polluting the global namespace. This is
not as bad as it sounds, since all delegation methods are marked private,
just like Ruby’s built-in global methods, like puts
. But still, especially if you ship your
Sinatra application with a Gem, you might want to avoid this. Another use
case is combining multiple Sinatra applications in a single process or using
Sinatra as Rack middleware. You can, of course, combine a classic
application with a modular one, but there can only be one classic
application per Ruby process.
Note
Sinatra actually jumps through some hoops to not break your objects
in classic style. For example, if you implement a method_missing
proxy (i.e. catch all methods
with method_missing
and delegate those
calls to another object) and you implement respond_to?
properly, the Sinatra DSL methods
will not be triggered and method_missing
will be called instead.
Creating a subclass itself should not be that hard, but what about
those DSL methods? In Chapter 4 we observed that method calls with an
implicit receiver are actually sent to self
. Now, inside a class body, self
is actually the class itself. Therefore we
can simply use the Sinatra DSL inside the class body. Make sure you only
require 'sinatra/base'
to avoid
activating the Top Level DSL unintentionally. See Example 4-1.
Example 4-1. Creating your own Subclass
require "sinatra/base" class MyApp < Sinatra::Base get '/' do "Hello from MyApp!" end end
We could also define routes from outside that class body, as shown in Example 4-2, but that is rather uncommon.
Example 4-2. Routes outside of the class body
require "sinatra/base" class MyApp < Sinatra::Base; end MyApp.get '/' do "Hello from MyApp!" end
It seems easy so far, but if you save that code in a Ruby file and
run it, nothing happens. If you replace require
"sinatra/base"
with require
"sinatra"
it will actually start a web server. But our route
is missing. Think about it, require
"sinatra"
will start a server for Sinatra::Application
, not for MyApp
.
Let’s figure out what Sinatra is doing to start a server for a classic application. Taking a look at lib/sinatra.rb, shown in Figure 4-1, in the Sinatra repository quickly reveals that all it’s doing is loading lib/sinatra/base.rb and lib/sinatra/main.rb. Since the server does not start when we load base.rb, that logic is probably somewhere in main.rb.
Skimming through the code, you might notice the at_exit
block. This is a hook offered by
Ruby. Any block passed to at_exit
will be called right before the Ruby program is going to exit. Sinatra
wraps that logic there to allow us to actually define routes before
starting the server. We don’t really need this for our modular
application. Since we are going to start the server explicitly anyway,
we can simply do so after defining our routes. As you might have
already guessed, run!
will start a
server. See Example 4-3.
Example 4-3. Serving a modular application with run!
require "sinatra/base" class MyApp < Sinatra::Base get '/' do "Hello from MyApp!" end run! end
Sinatra wants to make sure that the server really only starts
when appropriate. Therefore it makes sure to run only if the Ruby file
was executed directly and if no unhandled exception occurred. The
exception handling doesn’t matter for us, since we don’t trigger the
server from an at_exit
hook. Ruby
won’t reach the line with run!
on
it if there has been an exception. We should check if the file has
been executed directly, otherwise code used for testing, rackup
, or anything similar won’t be able to
load our application. Example 4-4 demonstrates how we
can make this type of check.
Most deployment scenarios probably require a config.ru
. We have already looked into this
in Chapter 3 and it should be pretty straightforward to
write and run such a configuration. Just use the code in Example 4-5 and launch the server with rackup -s thin -p 4567
.
Before we examine more advanced features of modular application,
let’s investigate settings for a moment. We’ve
already used settings in Chapter 2. You can write settings
at class or top level with set :key,
'value'
. It is now possible to access those via the settings
object.
Note
You can also use enable :key
(see Example 4-6) and disable
:key
, which are syntactic sugar for set :key, true
and set :key, false
respectively.
Example 4-6. Reading and writing settings
require 'sinatra' set :title, "My Website" # configure let's you specify env dependent options configure :development, :test do enable :admin_access end if settings.admin_access? get('/admin') { 'welcome to the admin area' } end get '/' do "<h1>#{ settings.title }</h1>" end
Another short code survey reveals that settings
is actually just an alias for the
current application class. Moreover, settings
is available both as class and as
instance method as shown in Example 4-7. Figure 4-2 shows the list of default settings available
to Sinatra.
Example 4-7. Definition of settings in lib/sinatra/base.rb
# Access settings defined with Base.set. def self.settings self end # Access settings defined with Base.set. def settings self.class.settings end
Without having to look at the code, it should become apparent
that set
is just some nice syntax
for defining methods on the application class. In fact, set
may also take a block instead of the
value defining a method from that block; see Example 4-8
for a demonstration.
Note
We’ve already seen something similar happening to route blocks
in Chapter 3. And indeed, Sinatra is using define_method
once again, but this time to
define class methods instead of instance methods.
Example 4-8. Playing with set in IRB
[~]$ irb ruby-1.9.2-p180 > require 'sinatra/base' => true ruby-1.9.2-p180 > class MyApp < Sinatra::Base; end => nil ruby-1.9.2-p180 > MyApp.settings => MyApp ruby-1.9.2-p180 > MyApp.set :foo, 42 => MyApp ruby-1.9.2-p180 > MyApp.foo => 42 ruby-1.9.2-p180 > MyApp.foo? => true ruby-1.9.2-p180 > MyApp.set(:bar) { rand < 0.5 ? false : foo } => MyApp ruby-1.9.2-p180 > MyApp.bar => false ruby-1.9.2-p180 > MyApp.bar => 42
Mapping everything to methods and embracing Ruby’s object model makes Sinatra classes extremely flexible. Following the main Sinatra principle of enabling flexibility by embracing simplicity, robustness, and through-and-through clean code becomes once again visible when creating subclasses of subclasses. Since settings are directly mapped to methods, those are inherited just like normal methods, as shown in Example 4-9.
Example 4-9. Settings and inheritance
[~]$ irb ruby-1.9.2-p180 > require 'sinatra/base' => true ruby-1.9.2-p180 > class GeneralApp < Sinatra::Base; end => nil ruby-1.9.2-p180 > class CustomApp < GeneralApp; end => nil ruby-1.9.2-p180 > GeneralApp.set :foo, 42 => MyApp ruby-1.9.2-p180 > GeneralApp.foo => 42 ruby-1.9.2-p180 > CustomApp.foo => 42 ruby-1.9.2-p180 > CustomApp.set :foo, 23 => 23 ruby-1.9.2-p180 > CustomApp.foo => 23 ruby-1.9.2-p180 > GeneralApp.foo => 42
Not only settings, but every aspect of a Sinatra class will be inherited by its subclasses. This includes defined routes, all the error handlers, extensions, middleware, and so on. But most importantly, it will be inherited just the way methods are inherited. In case you should define a route for a class after having subclassed that class, the route will also be available in the subclass. Yet, just like methods defined in subclasses, routes in subclasses precede routes defined in the superclass, no matter when those have been defined; see Example 4-10 for a demonstration of subclassing.
Example 4-10. Inherited routes
require 'sinatra/base' class GeneralApp < Sinatra::Base get '/about' do "this is a general app" end end class CustomApp < GeneralApp get '/about' do "this is a custom app" end end # This route will also be available in CustomApp GeneralApp.get '/' do "<a href='/about'>more infos</a>" end CustomApp.run!
Sinatra does not impose any application architecture on you but opens up a lot of possibilities. For instance, you can use inheritance to build a more complex controller architecture. Let’s take some inspiration from Rails controllers and start with a general application controller all other controllers inherit from.
Let’s create a boiler plate template for a Sinatra application with a controllers, helpers, and views directory. If you’d add a models directory, you’d be ready to go for Rails-style MVC. An example directory structure can be seen in Figure 4-3.
Just like Rails, we start with an ApplicationController
class all the other
controllers can inherit from. In that controller we’ll set up the
views folder, enable logging for all environments
but the test
environment, set up a
global helpers module we’ll define elsewhere, and add a not_found
handler all other controllers will
inherit. Example 4-11 sets up the foundation for our
class.
Example 4-11. controllers/application_controller.rb
class ApplicationController < Sinatra::Base helpers ApplicationHelper # set folder for templates to ../views, but make the path absolute set :views, File.expand_path('../../views', __FILE__) # don't enable logging when running tests configure :production, :development do enable :logging end # will be used to display 404 error pages not_found do title 'Not Found!' erb :not_found end end
You might have noticed the title
method used in the error handler.
That’s not part of Sinatra, so let’s implement it in the ApplicationHelper
as shown in Example 4-12.
Example 4-12. helpers/application_helper.rb
module ApplicationHelper def title(value = nil) @title = value if value @title ? "Controller Demo - #{@title}" : "Controller Demo" end end
We can use this method to set and retrieve the current page title in both the controllers and the views. Since we have the views path set up properly, we can create a layout.erb that will be used to wrap all other ERB templates.
Example 4-13. views/layout.erb
<html> <head> <title><%= title %></title> </head> <body> <%= yield %> </body> </html>
And a not_found.rb used for 404 error pages.
Example 4-14. views/not_fosvund.erb
Page does not exist! Check out the <a href='/example'>example page</a>.
We’re nearly done, we’ll just add an ExampleController
so we have something to
play with; Example 4-15 shows how to create a subclassed
controller.
Example 4-15. controllers/example_controller.rb
class ExampleController < ApplicationController get '/' do title "Example Page" erb :example end end
And we should create a corresponding view, shown in Example 4-16.
Now the real question is how to run it. We should go for using a
config.ru, since the Rack DSL offers a third
method besides use
and run
: map
.
This nifty method allows you to map a given path to a Rack endpoint.
We can use that to serve multiple Sinatra apps from the same process;
Example 4-17 shows how to do so.
Example 4-17. config.ru
require 'sinatra/base' Dir.glob('./{helpers,controllers}/*.rb').each { |file| require file } map('/example') { run ExampleController } map('/') { run ApplicationController }
Rack will remove the path supplied to map
from the request path and store it
safely in env['SCRIPT_NAME']
.
Sinatra’s url
helper will pick it
up to construct correct links for you.
Sometimes you might want to generate new Sinatra applications on
the fly without having to create a new constant. A typical example is
testing your application or Sinatra extension, but there are lots of
other use cases. You can simply use Sinatra.new
to create an anonymous, modular
application as shown in Example 4-18.
Example 4-18. Using Sinatra.new in a config.ru
require 'sinatra/base' app = Sinatra.new do get('/') { 'Hello World!' } end run app
Just like when creating constants, you may choose to use a different superclass to inherit from. Simply pass that class in as argument.
Example 4-19. Using a different superclass
require 'sinatra/base' general_app = Sinatra.new { enable :logging } custom_app = Sinatra.new(general_app) do get('/') { 'Hello World!' } end run custom_app
You can use this to dynamically generate new Sinatra applications.
Example 4-20. Dynamically generating Sinatra applications
require 'sinatra/base' words = %w[foo bar blah] words.each do |word| # generate a new application for each word map "/#{word}" { run Sinatra.new { get('/') { word } } } end map '/' do app = Sinatra.new do get '/' do list = words.map do |word| "<a href='/#{word}'>#{word}</a>" end list.join("<br>") end end run app end
In a typical scenario for modular applications, you usually embrace the usage of Rack: setting up different endpoints, creating your own middleware, and so on. If you want to use Sinatra in there as much as possible, you will have a hard time trying to only use a classic style application. If you decide to not only use Rack to communicate to the web server, but also internally to achieve a modular and flexible architecture, Sinatra will try to help you wherever possible.
In return it will give you interoperability and open up a variety of already existing libraries and middleware, just waiting for you to use them.
We already talked about using map
to serve more than one Sinatra application
from the same Rack handler. But this is not the only way to combine
multiple apps.
If you followed along in Chapter 3 closely, you might have arrived at this next point intuitively. We demonstrated how to use a Sinatra application as middleware. You are certainly free to use one Sinatra application as middleware in front of another Sinatra application. This will first try to find a route in the middleware application, and if that middleware application does not find a matching route, it will hand the request on to the other application, as shown in Example 4-21.
Example 4-21. Using Sinatra as endpoint and middleware
require 'sinatra/base' class Foo < Sinatra::Base get('/foo') { 'foo' } end class Bar < Sinatra::Base get('/bar') { 'bar' } use Foo run! end
This allows us to create a slightly different class
architecture, where classes are not responsible for a specific set of
paths, but instead may define any routes. If we combine this with
Ruby’s inherited
hook for
automatically tracking subclass creation (as in Example 4-22), we don’t even have to keep a list of classes
around.
Example 4-22. Automatically picking up subclasses as middleware
require 'sinatra/base' class ApplicationController < Sinatra::Base def self.inherited(sublass) super use sublass end enable :logging end class ExampleController < Sinatra::Base get('/example') { "Example!" } end # works with dynamically generated applications, too Sinatra.new ApplicationController do get '/' do "See the <a href='/example'>example</a>." end end ApplicationController.run!
Caution
If you define inherited
on
a Ruby class, always make sure you call super
. Sinatra uses inherited
, too, in order to set up a new
application class properly. If you skip the super
call, the class will not be set up
properly.
There is an alternative that seems rather similar at first glance: using a cascade rather than a middleware chain. It works pretty much the same. You supply a list of Rack application, which will be tried one after the other, and the first result that doesn’t have a status code of 404 will be returned. For a basic demonstration, Example 4-23 will behave exactly like a middleware chain.
Example 4-23. Using Rack::Cascade with rackup
require 'sinatra/base' class Foo < Sinatra::Base get('/foo') { 'foo' } end class Bar < Sinatra::Base get('/bar') { 'bar' } end run Rack::Cascade, [Foo, Bar]
There are a few minor differences to using middleware. First of
all, the behavior of passing on the request if no route matches is
Sinatra specific. With a cascade, you can use any endpoints; you might
first try a Rails application and a Sinatra application after that.
Moreover, imagine you explicitly return a 404 error from a Sinatra
application, for instance with get('/') {
not_found }
. If you do that in a middleware and the route
matches, the request will never be handed on to the second
application; with a cascade, it will be. See Example 4-24
for a concrete implementation of this concept.
Example 4-24. Handing on a request with not_found
require 'sinatra/base' class Foo1 < Sinatra::Base get('/foo') { not_found } end class Foo2 < Sinatra::Base get('/foo') { 'foo #2' } end run Rack::Cascade, [Foo1, Foo2]
Note
If you happen to have a larger number of endpoints, using a cascade is likely to result in better performance, at least on the official Ruby implementation.
Ruby uses a Mark-And-Sweep Garbage Collector to remove objects from memory that are no longer needed (usually it’s just called the GC), which will walk through all stack frames to mark objects that are not supposed to be removed. Since a middleware chain is a recursive structure, each middleware will add at least one stack frame, increasing the amount of work the GC has to deal with.
Since Ruby’s GC also is a Stop-The-World GC, your Ruby process will not be able to do anything else while it is collecting garbage.
A third option is using a Rack router.
We’ve already used the most simple router a few times: Rack::URLMap
. It ships with the rack
gem and is used by Rack under the hood
for its map
method. However, there
are a lot more routers out there for Rack with different capabilities
and characteristics. In a way, Sinatra is a router, too, or at least
can be used as such, but more on that later.
A router is similar to a Rack middleware. The main difference is
that it doesn’t wrap a single Rack endpoint, but keeps a list of
endpoints, just like Rack::Cascade
does. Depending on some criteria, usually the requested path, the
router will then decide what endpoint to hand the request to. This is
basically the same thing Sinatra does, except that it doesn’t hand off
the request. Instead, it decides what block of code to
evaluate.
Most routers differ in the way they decide which endpoint to
hand the request to. All routers meant for general usage do offer
routing based on the path, but how complex their path matching might
be varies. While Rack::URLMap
only
matches prefixes, most other routers allow simple wildcard matching.
Both Rack::Mount
, which is used by
Rails, and Sinatra allow arbitrary matching logic.
However, such flexibility comes at a price: Rack::Mount
and Sinatra have a routing
complexity of O(n), meaning that in the
worst-case scenario an incoming request has to be matched against all
the defined routes. Usually this doesn’t matter much, though. We did
some experiments replacing the Sinatra routing logic with a less
capable version, that does routing in O(1), and
we didn’t see any performance benefits for applications with fewer
than about 10,000 routes.
Rack::Mount
is known to
produce fast routing, however its API is not meant to be used directly
but rather by other libraries, like the Rails routes DSL. Install it
by running gem install
rack-mount
. Example 4-25 demonstrates how to
use it.
Example 4-25. Using Rack::Mount in a config.ru
require 'sinatra/base' require 'rack/mount' class Foo < Sinatra::Base get('/foo') { 'foo' } end class Bar < Sinatra::Base get('/bar') { 'bar' } end Routes = Rack::Mount::RouteSet.new do |set| set.add_route Foo, :path_info => %r{^/foo$} set.add_route Bar, :path_info => %r{^/bar$} end run Routes
It also supports other criteria besides the path. For instance, you can easily send different HTTP methods to different endpoints, as in Example 4-26.
Example 4-26. Route depending on the verb
require 'sinatra/base' require 'rack/mount' class Get < Sinatra::Base get('/') { 'GET!' } end class Post < Sinatra::Base post('/') { 'POST!' } end Routes = Rack::Mount::RouteSet.new do |set| set.add_route Get, :request_method => 'GET' set.add_route Post, :request_method => 'POST' end run Routes
The application’s return value is an integral part of the Rack specification. Rack is picky on what you may return. Sinatra, on the other hand, is forgiving when it comes to return values. Sinatra routes commonly have a string value returned on the last line of the block, but it can also be any value conforming to the Rack specification. Example 4-27 demonstrates this.
Example 4-27. Running a Rack application with Sinatra
require 'sinatra' # this is a valid Rack program MyApp = proc { [200, {'Content-Type' => 'text/plain'}, ['ok']] } # that you can run with Sinatra get('/', &MyApp)
Besides strings and Rack arrays, it accepts a wide range of return values that look nearly like Rack return values. As the body object can be a string, you don’t have to wrap it in an array. You don’t have to include a headers hash either. Example 4-28 clarifies this point.
You can also push a return value through the wire any time using
the halt
helper, like in Example 4-29.
Example 4-29. Alternative return values
require 'sinatra' get '/' do halt [418, "I'm a tea pot!"] "You'll never get here!" end
With halt
you can pass the
array elements as separate arguments. This helper is especially useful
in filters (as in Example 4-30), where you can use it to
directly send the response.
Since Sinatra accepts Rack return values, you can use the return
value of another Rack endpoint, as shown in Example 4-31.
Remember: all Rack applications respond to call
, which takes the env
hash as argument.
Example 4-31. Using another Rack endpoint in a route
require 'sinatra/base' class Foo < Sinatra::Base get('/') { "Hello from Foo!" } end class Bar < Sinatra::Base get('/') { Foo.call(env) } end Bar.run!
We can easily use this to implement a Rack router. Let’s implement
Rack::Mount
from Example 4-31 with Sinatra instead, as shown in Example 4-32.
Example 4-32. Using Sinatra as router
require 'sinatra/base' class Foo < Sinatra::Base get('/foo') { 'foo' } end class Bar < Sinatra::Base get('/bar') { 'bar' } end class Routes < Sinatra::Base get('/foo') { Foo.call(env) } get('/bar') { Bar.call(env) } end run Routes
And of course, we can also implement the method-based routing easily, as shown in Example 4-33.
Let’s recall the two common ways to extend Sinatra applications: extensions and helpers. Both are usable just the way they are in classic applications. However, let’s take a closer look at them again.
Helpers are instance methods and therefore available both in route
blocks and views. We can still use the helpers
method to import methods from a module
or to pass a block with methods to it, just the way we did in Chapter 3. See Example 4-34.
Example 4-34. Using helpers in a modular application
require 'sinatra/base' require 'date' module MyHelpers def time Time.now.to_s end end class MyApplication < Sinatra::Base helpers MyApplication helpers do def date Date.today.to_s end end get('/') { "it's #{time}\n" } get('/today') { "today is #{date}\n" } run! end
However, in the end, those methods will become normal instance methods, so there is actually no need to define them specially. See Example 4-35.
Extensions generally add DSL methods used at load time, just like
get
, before
, and so on. Just like helpers, those
can be defined on the class directly. See Example 4-36 for
a demonstration of using class methods.
Example 4-36. Using class methods
require 'sinatra/base' class MyApplication < Sinatra::Base def self.get_and_post(*args, &block) get(*args, &block) post(*args, &block) end get_and_post '/' do "Thanks for your #{request.request_method} request." end run! end
Previously, we introduced a common pattern for reusable
extensions: you call Sinatra.register
Extension
in the file defining the extension, you just have to
require
that file, and it will work
automatically. This is only true for classic applications, we still have
to register the extension explicitly in modular applications, as seen in
Example 4-37.
Example 4-37. Extensions and modular applications
require 'sinatra/base' module Sinatra module GetAndPost def get_and_post(*args, &block) get(*args, &block) post(*args, &block) end end # this will only affect Sinatra::Application register GetAndPost end class MyApplication < Sinatra::Base register Sinatra::GetAndPost get_and_post '/' do "Thanks for your #{request.request_method} request." end run! end
Why this overhead? Automatically registering extensions for modular applications is not as appealing as it might appear at first glance. Modular applications usually travel in packs: if one application loads an extension, you don’t want to drag that extension into other application classes by accident.
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.