What You Need to Know When Working With External APIs in Rails

Feb 24, 2015 - 8 min read
What You Need to Know When Working With External APIs in Rails
Share

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.

Social Shares

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:

  1. 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.
  2. 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:

  1. 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.
  2. 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.

12 Project Ideas
Cezar Halmagean
Software development consultant with over a decade of experience in helping growing companies scale large Ruby on Rails applications. Has written about the process of building Ruby on Rails applications in RubyWeekly, SemaphoreCI, and Foundr.