O'Reilly logo

Sinatra: Up and Running by Konstantin Haase, Alan Harris

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Chapter 4. Modular Applications

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.

Subclassing Sinatra

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

Running Modular Applications

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.

Using run!

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.

main.rb on GitHub

Figure 4-1. main.rb on GitHub

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.

Example 4-4. Only start a server if the file has been executed directly

require "sinatra/base"

class MyApp < Sinatra::Base
  get '/' do
    "Hello from MyApp!"
  end

  # $0 is the executed file
  # __FILE__ is the current file
  run! if __FILE__ == $0
end

With rackup

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.

Example 4-5. config.ru for running a modular application

require "./my_app"
run MyApp

About Settings

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
A list of default settings is available in Sinatra’s README file

Figure 4-2. A list of default settings is available in Sinatra’s README file

Settings and Classes

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

Subclassing Subclasses

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

Route Inheritance

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!

Architecture

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.

Example directory listing

Figure 4-3. Example directory listing

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.

Example 4-16. controllers/example_controller.rb

<h1>This is an example page!</h1>
map makes the ExampleController available

Figure 4-4. map makes the ExampleController available

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.

Dynamic Subclass Generation

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

Better Rack Citizenship

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.

Chaining Classes

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.

Middleware Chain

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.

Cascade

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.

With a Router

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

On Return Values

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.

Example 4-28. Alternative return values

require 'sinatra'

get('/') { [418, "I'm a tea pot!"] }

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.

Example 4-30. Alternative return values

require 'sinatra'

before { halt 418, "I'm a tea pot!" }
get('/') { "You'll never get here!" }

Using Sinatra as Router

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.

Example 4-33. Verb based routing with Sinatra

require 'sinatra/base'

class Get < Sinatra::Base
  get('/') { 'GET!' }
end

class Post < Sinatra::Base
  post('/') { 'POST!' }
end

class Routes < Sinatra::Base
  get('/') { Get.call(env) }
  post('/') { Post.call(env) }
end

run Routes

Extensions and Modular Applications

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

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.

Example 4-35. Helpers are just instance methods

require 'sinatra/base'

class MyApplication < Sinatra::Base
  def time
    Time.now.to_s
  end

  get('/') { "it's #{time}\n" }
  run!
end

Extensions

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.

Summary

We introduced modular applications in this chapter, which allows us to easily build more complex and flexible architectures. We discussed how to run and combine such applications and while doing so, learned a few more things about Rack.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required