Sometimes, you will want to decouple your Rails app and place your API in its own special folder in lib. You could be using Sinatra, Grape, or even your own Rack app. The advantage is you can separate out your API code, making it just that much easier to port over to another app. This guide is not intended to show you how to extract an API from your code, but how to throttle an API that has already been extracted. For some tips on how you can mount your API in your Rails app, take a look at the Inductor blog for a quick intro.

Now that you’ve skimmed the basics of mounting your Rack app (e.g. API) in your Rails app without touching your rackup file, you’re all set for the next step: throttling! testing!

Let’s go over a quick example with Cucumber. Don’t worry, I’ll keep it simple.

Go ahead and install cucumber-rails and database_cleaner into your Gemfile, if you aren’t using them already.

Note: You can use anything for your Rack app here, Sinatra, Grape, etc. as long as it returns a 200 status code for its base path. If you want to follow along, you can use this dumb rack app. There’s a link to a Github repo at the end of the post.

Here it is, a simple Rack app (a proc!) mounted in your Rails app at “/api”. It always responds “OK”, with status code 200.

# routes.rb
RailsApp::Application.routes.draw do
  mount proc { |env|
    [200, {}, ["OK"]]
  } => "/api"

  # your other Rails routes
end

At this point, you can start up the Rails server and hit your “API” at http://localhost:3000/api. You should see the word “OK” — that means it’s running. You just created a Rack app in your Rails app. Wasn’t that easy?

Now for the cuking. A simple feature to start off:

# features/my_dumb_api.feature
Feature: My Dumb API
  In order to retrieve an API response
  As a web API developer
  I want an API to respond to my requests

  Scenario: API is available
    When I send a GET request for "http://example.com/api/"
    Then the response code should be "200"

And some step definitions:

When /^I send a GET request for "([^"]*)"$/ do |path|
    get path
end
Then /^the response code should be "([^"]*)"$/ do |code|
    last_response.status.should == code.to_i
end

Now give cucumber a run.

$ cucumber features/my_dumb_api.feature

Using the default profile...
# features/my_dumb_api.feature
Feature: My Dumb API
  In order to retrieve an API response
  As a web API developer
  I want an API to respond to my requests

  Scenario: API is available                                # features/my_dumb_api.feature:7
    When I send a GET request for "http://example.com/api/" # features/step_definitions/api_steps.rb:1
    Then the response code should be "200"                  # features/step_definitions/api_steps.rb:5

1 scenario (1 passed)
2 steps (2 passed)
0m0.154s

Excellent! It picked it up right away. Now we can start thinking about throttling. Jump over to your Gemfile and add rack-throttle:

gem 'rack-throttle', :require => 'rack/throttle'

Update your bundle. Now, let’s start with the Cucumber feature this time.

Feature: My Dumb API
  In order to retrieve an API response
  As a web API developer
  I want an API to respond to my requests

  Scenario: API is available
    When I send a GET request for "http://example.com/api/"
    Then the response code should be "200"

  Scenario: Exceeding API Query Rate
    When I send more than one GET request in a second to "http://example.com/api"
    Then the response code should be "403"

And it’s corresponding steps file:

When /^I send a GET request for "([^"]*)"$/ do |path|
    get path
end
When /^I send more than one GET request in a second to "([^"]*)"$/ do |path|
    # We'll assume this happens in < 1 second
    get path
    get path
end

Then /^the response code should be "([^"]*)"$/ do |code|
    last_response.status.should == code.to_i
end

If we run this through Cucumber, it fails, because we haven’t done throttling yet. Jump to your routes file, and switch it to:

RailsApp::Application.routes.draw do
  mount Rack::Builder.new {
    use Rack::Throttle::Interval, :min => 1.0
    run proc { |env|
      [200, {}, ["OK"]]
    }
  } => "/api"
end

There, we just built a Rack app with middleware (the Rack::Throttle line), which defers to our Rack app (the proc) when the middleware passes the request onwards. Now when you run Cucumber, everything passes! You may think you’re ready to Cuke out the rest of your API, but you’re about to hit a roadblock — throttling hits all your Cucumber features. I have considered two ways to deal with this:

  1. Stub out Rack::Throttle and tell it which features you specifically want to throttle using Cucumber tags.
  2. Use a separate Rack app for Cucumber testing, and turn throttling on for certain features with Cucumber tags.

I chose the second option. First, I added the @no-throttle tag to the scenarios where throttling was not relevant:

Feature: My Dumb API
  In order to retrieve an API response
  As a web API developer
  I want an API to respond to my requests

  @no-throttle
  Scenario: API is available
    When I send a GET request for "http://example.com/api/"
    Then the response code should be "200"

  Scenario: Exceeding API Query Rate
    When I send more than one GET request in a second to "http://example.com/api"
    Then the response code should be "403"

And then a before filter in the steps file:

Before "@no-throttle" do
  @app = Rack::Builder.new {
    map "/api" do
      run proc { |env|
        [200, {}, ["OK"]]
      }
    end
  } 
end

When /^I send a GET request for "([^"]*)"$/ do |path|
    get path
end
When /^I send more than one GET request in a second to "([^"]*)"$/ do |path|
    # We'll assume this happens in < 1 second
    get path
    get path
end 
      
Then /^the response code should be "([^"]*)"$/ do |code|
    last_response.status.should == code.to_i
end

Now when you run your Cucumber features, the unthrottled scenarios will use a separate rack app, mounted without throttling. The downside to this method is that your have to keep the Rack app in api_steps.rb up to date with the one in routes.rb. A small price to pay, but less work than stubbing out Rack::Throttle.

You can browse the source code for this example Rails app on Github.