Consuming the GitHub API
--
One of the main components of the module three curriculum at Turing is APIs. We jumped in last week with a three day project to effectively replicate the home page for a user of a service of our choosing — I chose GitHub. This is what I did and learned, as well as my thoughts on ways I could improve upon it.
OAuth
Because a user’s GitHub statistics require authorization, user’s were required to sign into my site through GitHub. I used the OmniAuth gem to do this, which made the process pretty painless. The gem has strategies for a pretty exstensive list of services. When a user is logged in using OmniAuth, you are given a hash of information provided by the service that you can access by calling request.env[“omniauth.auth”]. I used this to save the information I needed into the database (user information was the only thing I stored in a database, the rest was accessed through API calls.) The first time a user logged into my app, they were created something like this:
class User < ActiveRecord::Base def self.create_with_omniauth(auth)
create! do |user|
user.provider = auth["provider"]
user.uid = auth["uid"]
user.name = auth["info"]["name"]
user.token = auth["credentials"]["token"]
user.nickname = auth["info"]["nickname"]
user.image = auth["extra"]["raw_info"]["avatar_url"]
end
end
end
Some of this didn’t need to be saved to the database, but sped things up a little. The most important piece was the token. More on that.
Making API Calls
GitHub has awesome documentation for their API. To access almost any information about a user, authentication is necessary this is where the token mentioned above comes in. It is unique to a user and used in the headers of your request. Here’s what a whole request looks like:
class GithubService
attr_reader :current_user def initialize(current_user)
@current_user = current_user
@_connection = Faraday.new("https://api.github.com")
connection.headers['Authorization'] = "token #{current_user.token}"
end def repos
get("/users/#{current_user.nickname}/repos")
end private
def get(path)
JSON.parse(connection.get(path).body)
end def connection
@_connection
end
end
Let’s look through that. (There was more to it, of course, the repos method represents just one API call of many, but the rest of the methods shown there were reused by all of them.)
I used Faraday as my HTTP client, and passed it the root URI for the github API, and put the authorization required by GitHub in the headers, accessing the token saved in the database. The private get method specifies the HTTP verb used by calling connection.get, but others could be passed. Each method then returned a parsed version of the response body (which comes through as JSON).
Presenting the Data
This step evolved quite a bit over the course of the project. The most basic way to show the data is to pass a view an instance of the service shown above, and make the API call right in the view, and access the information you need from the hash right there. This is problematic for obvious reasons. My final iteration looked something like this (working backwards):
- When the data was ready, hide the spinning wheel that was in the section of the page where the data belonged, and access the information at the route specified in the AJAX call:
$(document).ready(function(){
renderRepos();
}); var renderRepos = function() {
$.ajax({
url: '/repositories',
success: function(data) {
var items = [];
$.each(data, function(key, repository) {
items.push( "<p>" + repository.table.name + "</p>" );
})
showHide('#all_repos', '#repo-wheel', items);
}
});
} var showHide = function(divId, wheelId, items) {
$(divId).html(items.join(""));
$(wheelId).addClass('hidden');
}
- Found at the “/repositories” route was this:
class RepositoriesController < ApplicationController
def index
@repositories = Repository.all(current_user)
render json: @repositories
end
end
- Calling Repository.all accessed the Repository PORO (not an ActiveRecord model), which looked like this:
class Repository < OpenStruct
def self.service(current_user)
GithubService.new(current_user)
end def self.all(current_user)
repos_hash = service(current_user).repos
repos_hash.map do |repo_hash|
self.new(repo_hash)
end
end
end
Having the PORO inherit from OpenStruct allowed for simple creation of an object with methods on it for each key of the hash passed in (on line 10).
- And that brings us full circle back to the service and the Faraday call shown above!
Other Features
The pattern shown above was repeated for most of the data shown on the page. I did end up implementing the ability for a user to both create and delete gists from my app, because I wanted to practice methods beyond get, but the patterns were similar, with additional create and delete methods, along with a few helpers, on the Gist PORO.
Improvements
For a three day project and my first API, I felt pretty good about what I accomplished, but here are some thoughts on things I would have liked to do better given more time, or on my next project:
- Caching: Even with AJAX calls, the site loaded pretty slowly. Since there was one landing page for everything beyond the gist functionality, it required quite a few API calls, as well as the Nokogiri scraping. I need to think more about the balance between performance, and being up to date — the information on someone’s GitHub page changes pretty frequently.
- Background workers: for the reason mentioned above. I don’t know much about how background workers work, but I would like to read up on it.
- Move away from the use of OpenStructs to POROs with only the attributes I actually need, that I define.
- Updated my AJAX calls to use the $.getJSON method. I’m glad I did it the way I did this time around so I have a better understanding of what is happening (and to practice my JavaScript!), but the jQuery route would be cleaner.