What You Need to Know When Working With External APIs in Rails
Accessing APIs seems to be pretty straight forward, until you actually do it. There are a few gotchas that I’d like to talk about to make your experience in working with external APIs a little bit more pleasant.
What is an API
Wikipedia defines an API as: a set of routines, protocols, and tools for building software applications. That’s all good but we want something easier to understand.
An API (Application Programming Interface) is somewhat like a black box that you can fetch data from and send data to. APIs have their own (sometimes very complex) functionality that they share with the world, so developers can make use of that functionality in their own applications.
Take the Google Maps’ API for example. With the Google Maps API you can give it a city name and the API will give you back it’s lat/lng coordinates (and a bunch more info). And all that can be done programatically, you don’t have to click and copy anything. How cool is that!?
Here are a few examples of well known and very useful APIs to better illustrate what an API is:
With the Facebook API you can like pages, sign in users using their facebook accounts, write on their timeline, etc. With the Rotten Tomatoes API you probably want to get a rating for a movie. With the Twitter API you can fetch, post tweets, and a ton of other things.
These are just some basic examples for you to get an idea about why you would want to use or build an API.
Query-ing an API
Now that you’ve got a million ideas about how you could fetch different data from all the APIs of the world, let’s see how you can actually do it inside your Ruby on Rails application.
We’ve stumbled upon a stupid API related bug the other day with our blog which I think will serve as a good example here.
The problem was, we were building our social share box (reinventing the wheel of course, cause that’s how we roll) which lists the number of shares an article has on various social networks.
We’ve decided to use the social_shares gem which does a great job of checking how many times an url has been shared on social media.
So the communication with the API is tucked away in this gem so we don’t have to worry about the details. What we do want though is to know that the adapter library does it’s thing right. That is something we can make sure by using an integration test.
Start with the integration test
Feature: Display the total number of shares
In order to share the current article
As a user
I want to see how many people shared it before me
Scenario: User sees the total number of shares
Given I have an article url I want to check
When I go to my social shares page
Then I should see the total number of shares
Given(/^I have an article url I want to check$/) do
@url = "http://mixandgo.com/learn/the-beginner-s-guide-to-rails-helpers"
end
When(/^I go to my social shares page$/) do
VCR.use_cassette("total_shares") do
visit root_path(:url => @url)
end
end
Then(/^I should see the total number of shares$/) do
expect(page.text).to match(/^\d+\stotal shares/)
end
This test will serve as a safety net, meaning we want to be sure that our application and the third party library are working well together. We also want to know that the application still works if the third part library changes, for example if you want to upgrade the gem to a newer version.
Dive in, write the unit test
So to start we’ll need some kind of adapter that will serve as a proxy between our application and the social network stuff (the socialshares gem in this case). So let’s create _a test for what we’d want our code to look like.
I’m thinking maybe some class called SocialAdapter
could be a good name for a proxy class. So let’s create a spec/lib/social_adapter_spec.rb
file that will hold our first unit test.
require 'spec_helper'
require 'social_adapter'
describe SocialAdapter do
let(:test_url) { "http://example.com/cool-post" }
let(:social_col) { double }
before :each do
networks = [:facebook, :twitter]
allow(social_col).to receive(:total).with(test_url, networks).and_return(21)
end
describe "#total_shares_for" do
it "returns the total number of shares for a given url" do
sa = SocialAdapter.new(social_col)
expect(sa.total_shares_for(test_url)).to eq(21)
end
end
end
And here’s the implementation.
class SocialAdapter
attr_reader :social_proxy
def initialize(social_proxy=nil)
@social_proxy = social_proxy
end
def total_shares_for(url)
social_proxy.total(url, social_networks)
end
private
def social_networks
[:facebook, :twitter]
end
end
The design choice of this implementation comes from trying to minimize coupling with the SocialShares
class that comes from the gem. So passing a SocialShares
object into the adapter means we’re injecting dependencies into our class.
Here are some of the benefits of injecting dependencies instead of hard coding the class name:
- Our class doesn’t have to know the name of the collaborator. It just needs it to respond to the
total
method, and pass two arguments to it. - We can reuse the adapter, say a new library comes along that’s more shiny and it too responds to
total
(with the two arguments). We can just pass it the other object and it would still work.
Moving on to our controller
Our unit test is passing now but we’re not done yet. If we’re running the integration test, we’re gonna get a failure. That is because we’re not using the adapter class anywhere, we’ve just defined it.
So let’s put it to work. In our controller we’re gonna store the result of the adapter’s total_shares_for
method into an instance variable that we’ll later use in the view.
But first, we’ll need a test. If you want to learn more about testing controllers, please refer to the controller testing post.
require 'rails_helper'
require 'social_adapter'
RSpec.describe SiteController, type: :controller do
describe "GET index" do
let(:url) { "http://example.com/funky-blog-post" }
let(:social_adapter) { double }
before :each do
allow(SocialAdapter).to receive(:new).with(SocialShares).and_return(social_adapter)
end
it "gets the total shares for a given url" do
expect(social_adapter).to receive(:total_shares_for).with(url)
get :index, :url => url
end
it "assigns the SocialAdapter total" do
allow(social_adapter).to receive(:total_shares_for).
with(url).and_return(21)
get :index, :url => url
expect(assigns(:social_shares)).to eq(21)
end
end
end
require 'social_shares'
require 'social_adapter'
class SiteController < ApplicationController
def index
social_adapter = SocialAdapter.new(SocialShares)
@social_shares = social_adapter.total_shares_for(params.require(:url))
end
end
Caching
We could stop right here and it would be just fine, well… not really. Remember about the bug I was telling you? Here’s what happens if we stop right here: Every time someone visits our page, we make an api request for each of the selected social networks.
That is bad, really bad, because:
- The social API call blocks the loading of the page (since we’re not using AJAX) and so our readers need to wait for each API to finish it’s thing before they can read the article.
- We’re at the APIs mercy, meaning if they decide to only let 100 requests per day from a given client, our application will raise an exception.
So in order to fix this mess we need to cache the request and only let it refresh once every few hours. That way, we’re always going to have something to show to the user and not rely on the API for every page load.
Testing, caching, fun
Like every good Rails citizen, you wanna write your test case first right? So how do you test caching?
Given this is a controller test, I’m not going to get into too much cache testing here, just enough to get us going. We’re gonna use low-level caching to store the total number of shares.
require 'rails_helper'
require 'social_adapter'
RSpec.describe SiteController, type: :controller do
describe "GET index" do
let(:url) { "http://example.com/funky-blog-post" }
let(:social_adapter) { double }
before :each do
allow(SocialAdapter).to receive(:new).
with(SocialShares).and_return(social_adapter)
allow(social_adapter).to receive(:total_shares_for).
with(url).and_return(21)
Rails.cache.clear
end
it "gets the total shares for a given url" do
expect(social_adapter).to receive(:total_shares_for).with(url)
get :index, :url => url
end
it "assigns the SocialAdapter total" do
get :index, :url => url
expect(assigns(:social_shares)).to eq(21)
end
it "caches the result" do
rails_caching = double.as_null_object
allow(Rails).to receive(:cache).and_return(rails_caching)
expect(rails_caching).to receive(:fetch).with("total_shares", :expires_in => 2.hours)
get :index, :url => url
end
end
end
require 'social_shares'
require 'social_adapter'
class SiteController < ApplicationController
def index
@social_shares = get_total_shares
end
private
def get_total_shares
social_adapter = SocialAdapter.new(SocialShares)
Rails.cache.fetch("total_shares", :expires_in => 2.hours) do
social_adapter.total_shares_for(params.require(:url))
end
end
end
Now that’s much better. Once we fetch the number of shares from the social APIs, we’re caching it for two hours. Then, after two hours, we let the request to the API go through again and fetch a new value for another two hours.
Conclusion
It’s a lot better to cache interaction with external APIs if possible since you’re never going to know if that API will respond to your request, and you don’t want your application to depend on that. Having cached responses also improves response time since you’re not going over the network every time to get your data.