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:
Let’s go over a quick example with Cucumber. Don’t worry, I’ll keep it simple.
Go ahead and install
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.
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:
And some step definitions:
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.
And it’s corresponding steps file:
If we run this through Cucumber, it fails, because we haven’t done throttling yet. Jump to your routes file, and switch it to:
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:
- Stub out
Rack::Throttleand tell it which features you specifically want to throttle using Cucumber tags.
- 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:
And then a before filter in the steps file:
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
You can browse the source code for this example Rails app on Github.