---
title: Let's Build a Sinatra
teaser: Take a deep dive into Rack by building Sinatra from scratch.
tags: web,ruby
author: Gabe Berke-Williams
published_on: 2015-10-05
---

[Sinatra][sinatra-home] is a domain-specific language for quickly creating web
applications in Ruby. After using it on a few projects, I decided to find out
how it works under the hood.

Here's a step by step guide on how I wrote my own Sinatra.

[sinatra-home]: http://www.sinatrarb.com/

## What is Sinatra?

At its core, Sinatra is a Rack application.
I [already wrote about Rack][rack-post],
so if you're a little fuzzy on how Rack works,
that post is a great starting point.
Sinatra is a layer on top of Rack: it provides an excellent DSL for specifying what your Rack app responds to,
and what it sends back. For example, here's a Sinatra application:

[rack-post]: http://gabebw.com/blog/2015/08/10/advanced-rack

```ruby
get "/hello" do
  [200, {}, "Hello from Sinatra!"]
end

post "/hello" do
  [200, {}, "Hello from a post-Sinatra world!"]
end
```

We should be able to run the code above,
then send a GET to `/hello` on localhost and see "Hello from Sinatra!".
A POST to `/hello` should give us a snarky message about Sinatra.
And visiting any route that we haven't explicitly defined should give
us a 404.

## Technical architecture

After poring over [the Sinatra source][sinatra-base], I've distilled Sinatra down to a
simplified technical architecture.

[sinatra-base]: https://github.com/sinatra/sinatra/blob/master/lib/sinatra/base.rb

We'll make a base Sinatra class that other classes can inherit from. It will
store routes (like `GET /hello`) and actions to take when hitting those routes.
For each request, it will match the requested route to the stored routes, and
take action if there's a match, or return a 404 if nothing matches.

## OK let's actually build it

Let's call our version Nancy.

Here's the first iteration: a class that has a method `get` that takes a path
and a handler block.

```ruby
# nancy.rb
require "rack"

module Nancy
  class Base
    def initialize
      @routes = {}
    end

    attr_reader :routes

    def get(path, &handler)
      route("GET", path, &handler)
    end

    private

    def route(verb, path, &handler)
      @routes[verb] ||= {}
      @routes[verb][path] = handler
    end
  end
end
```

The `route` method takes a verb, a path, and a handler block.
It stores the handler in a nested hash of the verb and path,
which ensures that routes with the same path
like `POST /hello` and `GET /hello`
won't conflict.

Let's add this at the bottom to try it out:

```ruby
nancy = Nancy::Base.new

nancy.get "/hello" do
  [200, {}, ["Nancy says hello"]]
end

puts nancy.routes
```

Note that we currently have `nancy.get` instead of just `get`,
but don't worry, we'll fix that at the end.

If we run `ruby nancy.rb`, we see:

```ruby
{
  "GET" => {
    "/hello" => #<Proc:0x007fea4a185a88@nancy.rb:26>
  }
}
```

Cool! Calling `nancy.get` correctly adds a route.

## Nancy on Rack

Now let's make `Nancy::Base` a Rack app by adding a minimal `call` method,
as described in [my Rack post][call]:

[call]: http://gabebw.com/blog/2015/08/10/advanced-rack#the-rack-specification

```ruby
# nancy.rb
def call(env)
  @request = Rack::Request.new(env)
  verb = @request.request_method
  requested_path = @request.path_info

  handler = @routes[verb][requested_path]

  handler.call
end
```

First, we grab the verb and requested path (like `GET` and `/the/path`)
from the `env` parameter using `Rack::Request`.
Then we grab the handler block from `@routes` and call it.
We are assuming that end users will ensure their block handler will return
something that Rack can understand, which our block does.

Now that we've added a `call` method to `Nancy::Base`,
let's add a handler at the bottom:

```ruby
nancy = Nancy::Base.new

nancy.get "/hello" do
  [200, {}, ["Nancy says hello"]]
end

# This line is new!
Rack::Handler::WEBrick.run nancy, Port: 9292
```

Rack handlers take a Rack app and actually run them.
We're using WEBrick because it's built in to Ruby.

Run your file with `ruby nancy.rb` and visit http://localhost:9292/hello.
You should see a greeting.
Important future note: this code doesn't automatically reload,
so every time you change this file,
you'll need to hit <kbd>Ctrl-c</kbd> and run the code again.

## Handling errors

Visiting a route that we've defined shows a message, but visiting
a nonexistent route like http://localhost:9292/bad
shows a gross Internal Server Error page.
Let's show a custom error page instead.

To do that, we need to modify our `call` method a little bit.
Here's a diff:

```diff
 def call(env)
   @request = Rack::Request.new(env)
   verb = @request.request_method
   requested_path = @request.path_info

-  handler = @routes[verb][requested_path]
-
-  handler.call
+  handler = @routes.fetch(verb, {}).fetch(requested_path, nil)

+  if handler
+    handler.call
+  else
+    [404, {}, ["Oops! No route for #{verb} #{requested_path}"]]
+  end
 end
```

If our nested `@routes` hash doesn't have a `handler` defined
for the requested verb/path combination,
we now return a 404 with an error message.

## Getting information about the request in the handler

Our `nancy.get` handler always shows the same content.
But what if we want to use information about the request (like params) in our handler?
The [`Rack::Request`][rack-request] class that wraps the `env`
has a method called `params` that contains information about all parameters
provided to the method - GET, POST, PATCH, etc.

[rack-request]: http://www.rubydoc.info/gems/rack/Rack/Request

First, we need to add a `params` method to `Nancy::Base`:

```ruby
module Nancy
  class Base
    #
    # ...other methods....
    #

    def params
      @request.params
    end
  end
end
```

We still need to give our route handlers (the block that we pass to each `get`)
access to that `params` method, though.

## Access to params

We have a `params` method on the instance of `Nancy::Base`, so let's evaluate
our route handler block in the context of that instance, to give it access to
all of the methods. We can do that with `instance_eval`.
If you're a little fuzzy on `instance_eval`, try [this article on DSLs][dsl],
which goes into it in detail.

[dsl]: https://thoughtbot.com/blog/writing-a-domain-specific-language-in-ruby

Here's the change we need to make to the `call` method:

```diff
 if handler
-  handler.call
+  instance_eval(&handler)
 else
   [404, {}, ["Oops! Couldn't find #{verb} #{requested_path}"]]
 end
```

This is a little tricky, so I'm going to go over it in detail:

* The original `handler` is a "free floating" lambda, without any context
* If we call, uh, `call` on that handler it doesn't have access to any of the
  `Nancy::Base` instance's methods
* When we instead run the handler using `instance_eval`, the handler block is
  *run in the context of* the `Nancy::Base` instance, which means it has access
  to that instance's methods and instance variables

 Now we have access to `params` in the handler block. Try adding the following
 to `nancy.rb` and then visiting http://localhost:9292/?foo=bar&hello=goodbye:

```ruby
nancy.get "/" do
  [200, {}, ["Your params are #{params.inspect}"]]
end
```

Any other methods we add to `Nancy::Base`
will also be available inside route handler blocks.

## Supporting POST, PUT, etc

So far `nancy.get` works,
but we haven't defined methods for other common HTTP verbs yet.
The code is very similar to `get`:

```ruby
# nancy.rb
def post(path, &handler)
  route("POST", path, &handler)
end

def put(path, &handler)
  route("PUT", path, &handler)
end

def patch(path, &handler)
  route("PATCH", path, &handler)
end

def delete(path, &handler)
  route("DELETE", path, &handler)
end
```

In most POST and PUT requests, we'll want to access the request body.
Since the `handler` has access to every instance method on `Nancy::Base`,
we need to add an instance method named `request` that has access to our
`@request` instance variable that we set in `call`:

```ruby
attr_reader :request
```

After adding that, we can access the `request` in every handler block:

```ruby
nancy.post "/" do
  [200, {}, request.body]
end
```

Add that route, and now you can use `curl`
to send the contents of a file to Nancy
and she'll echo it back to you:

```bash
$ curl --data "body is hello" localhost:9292
body is hello
```

## Modern conveniences

Let's spruce up the place:

1. Handlers should be able to use `params` instead of `request.params`
1. If a handler returns a string, assume that it is a successful response

`params` is fairly easy, we can add a small method to `Nancy::Base`:

```ruby
def params
  request.params
end
```

For the second item, we need to check the result of the handler block in `call`:

```diff
   if handler
-    instance_eval(&handler)
+    result = instance_eval(&handler)
+    if result.class == String
+      [200, {}, [result]]
+    else
+      result
+    end
   else
     [404, {}, ["Oops! Couldn't find #{verb} #{requested_path}"]]
   end
```

Neat! If evaluating the block returns a String, we construct a successful Rack
response; otherwise, we return the result of the block as-is. Now we can do
this:

```ruby
nancy.get "/hello" do
  "Nancy says hello!"
end
```

## Delegating to Nancy::Application

That `nancy.get` is really getting me down.
It'd be really cool if we could just do `get`.
Here's how.

Our strategy will be to make a Sinatra class that we can access from anywhere,
then delegate `get`, `post`, etcetera to that class.
An example will explain the "access from anywhere":
every time we call `Nancy::Base.new`,
we get a new instance of `Nancy::Base`.
So if we add routes to `Nancy::Base.new`,
then in another file try running `Nancy::Base.new` with a Rack handler,
we'd be running a brand new instance that doesn't have any of our routes.

So let's define an instance of `Nancy::Base` that we can reference:

```ruby
module Nancy
  class Base
    # methods...
  end

  Application = Base.new
end
```

Try changing your routes to use `Nancy::Application`:

```ruby
nancy_application = Nancy::Application

nancy_application.get "/hello" do
  "Nancy::Application says hello"
end

# Use `nancy_application,` not `nancy`
Rack::Handler::WEBrick.run nancy_application, Port: 9292
```

That's step 1. Step 2 is to delegate methods to `Nancy::Application`.
Add the following code (taken [from Sinatra][delegator]) to `nancy.rb`:

[delegator]: https://github.com/sinatra/sinatra/blob/1d48ab2929614c679a4ed6cb827131ab10f1af05/lib/sinatra/base.rb#L1973-L1997

```ruby
module Nancy
  module Delegator
    def self.delegate(*methods, to:)
      Array(methods).each do |method_name|
        define_method(method_name) do |*args, &block|
          to.send(method_name, *args, &block)
        end

        private method_name
      end
    end

    delegate :get, :patch, :put, :post, :delete, :head, to: Application
  end
end
```

`Nancy::Delegator` will delegate `get`, `patch`, `post`, etc
to `Nancy::Application`
so that calling `get` in context with `Nancy::Delegator` will behave exactly like
calling `Nancy::Application.get`.

Now let's include it everywhere.
Add this line to `nancy.rb` outside of the `Nancy` module:

```ruby
include Nancy::Delegator
```

Now we can delete all of the `Nancy::Base.new` and `nancy_application` lines
and try the fancy new routes:

```ruby
get "/bare-get" do
  "Whoa, it works!"
end

post "/" do
  request.body.read
end

Rack::Handler::WEBrick.run Nancy::Application, Port: 9292
```

Plus it works when run with `rackup` via `config.ru`:

```ruby
# config.ru
require "./nancy"

run Nancy::Application
```

Here's the full final code:

```ruby
# nancy.rb
require "rack"

module Nancy
  class Base
    def initialize
      @routes = {}
    end

    attr_reader :routes

    def get(path, &handler)
      route("GET", path, &handler)
    end

    def post(path, &handler)
      route("POST", path, &handler)
    end

    def put(path, &handler)
      route("PUT", path, &handler)
    end

    def patch(path, &handler)
      route("PATCH", path, &handler)
    end

    def delete(path, &handler)
      route("DELETE", path, &handler)
    end

    def head(path, &handler)
      route("HEAD", path, &handler)
    end

    def call(env)
      @request = Rack::Request.new(env)
      verb = @request.request_method
      requested_path = @request.path_info

      handler = @routes.fetch(verb, {}).fetch(requested_path, nil)

      if handler
        result = instance_eval(&handler)
        if result.class == String
          [200, {}, [result]]
        else
          result
        end
      else
        [404, {}, ["Oops! No route for #{verb} #{requested_path}"]]
      end
    end

    attr_reader :request

    private

    def route(verb, path, &handler)
      @routes[verb] ||= {}
      @routes[verb][path] = handler
    end

    def params
      @request.params
    end
  end

  Application = Base.new

  module Delegator
    def self.delegate(*methods, to:)
      Array(methods).each do |method_name|
        define_method(method_name) do |*args, &block|
          to.send(method_name, *args, &block)
        end

        private method_name
      end
    end

    delegate :get, :patch, :put, :post, :delete, :head, to: Application
  end
end

include Nancy::Delegator
```

Here's an app that uses Nancy:

```ruby
# app.rb
# run with `ruby app.rb`
require "./nancy"

get "/" do
  "Hey there!"
end

Rack::Handler::WEBrick.run Nancy::Application, Port: 9292
```

And that's Nancy Sinatra! Let's review what we can do with this code:

* Write any Rack app, with a simpler interface: if Rack can do it, so can
  Nancy.
* We can use bare methods (`get` instead of `nancy.get`).
* We can subclass `Nancy::Base` to make our own custom apps.

## Further reading

Sinatra's source code is almost all in [`base.rb`][base].
It's dense, but more understandable after reading this post.
I'd start with the `call!` method; also check out the `Response` class,
which is a subclass of `Rack::Response`.
One thing to keep in mind is that Sinatra is class-based,
while Nancy is object-based;
where Nancy uses an instance-level `get` method, Sinatra uses a class-level
`get` method.

[base]: https://github.com/sinatra/sinatra/blob/master/lib/sinatra/base.rb
