Consuming HTTP APIs in Ruby
What is your favorite technique for consuming HTTP APIs in Ruby? I like using HTTParty!
It offers a simple, intuitive API.
It makes it easy to support many standard API authentication mechanisms.
It de-serializes responses based on content type headers.
It allows us to write simple wrappers that are very close in form to the API we want to communicate with.
It has a nice name! And we know how hard it is to name things.
I have come to employ a few patterns when working with HTTParty. They are all centered around having a convenient internal API and the ease of testing.
Most APIs I’ve worked with have one of the following authentication mechanisms:
HTTP Basic Authentication
HTTP Digest Authentication
Auth token in the header
API key in query params/request body
I am fond of examples, so let’s consider an example. A RESTful service, where we interact with the “Article” resource. We can list articles, get details about an article, and create, update & delete an article. The service demands HTTP Basic Auth, and JSON encoding.
1 class ArticlesService
2 include HTTParty
3
4 base_uri "https://api.example.com"
5 read_timeout 5 # always have timeouts!
6 # debug_output $stdout # for quick access during debugging
7
8 attr_reader :auth, :headers
9
10 def initialize
11 @auth = {
12 username: ENV["API_USERNAME"],
13 password: ENV["API_PASSWORD"],
14 }
15
16 @headers = {
17 "Content-Type" => "application/json",
18 }
19 end
20
21 def index
22 get("articles")
23 end
24
25 def show(article_id)
26 get("articles/#{article_id}")
27 end
28
29 def create(attributes)
30 self.class.post(
31 endpoint("articles"),
32 default_options.merge(body: attributes.to_json)
33 )
34 end
35
36 def update(article_id, attributes)
37 self.class.patch(
38 endpoint("articles/#{article_id}"),
39 default_options.merge(body: attributes.to_json)
40 )
41 end
42
43 def destroy(article_id)
44 self.class.delete(
45 endpoint("articles/#{article_id}"),
46 default_options
47 )
48 end
49
50 protected
51
52 def default_options
53 {
54 headers: headers,
55 basic_auth: auth,
56 }
57 end
58
59 def endpoint(uri)
60 "/v1/#{uri}"
61 end
62
63 def get(uri)
64 self.class.get(
65 endpoint(uri),
66 default_options
67 )
68 end
69 end
GitHub Gist Link for better readability:
Here’s why I like this code:
Simple, easy-to-read code that mimics the API quite nicely.
Intuitive. Sending a POST request is as simple as calling
post
. You don't need to worry about remembering multiple things. Specifying headers is simply passing an argument calledheaders
.Interacting with the API is now simple:
1 service = ArticlesService.new
2
3 pp service.index
4 # []
5
6 # Create an article
7 response = service.create(
8 name: "Star Trek: A new hope",
9 body: "A play about how Frodo is
10 tricked into attending the
11 tri-wizard tournament by
12 evil shogun Gandalf"
13 )
14
15 # Made a mistake in the title, update it
16 service.update(
17 response['article']['id'],
18 name: "Star Wars: Into the darkness"
19 )
20
21 # Delete that abomination of an article
22 service.delete(response['article']['id'])
Where’s the fun without ever-changing requirements?
API, now V2, demands that we use digest auth instead of basic auth.
1 def default_options
2 {
3 headers: headers,
4 digest_auth: auth,
5 }
6 end
7
8 def endpoint(uri)
9 "/v2/#{uri}"
10 end
That was a simple change. Let’s try adding custom headers. The API now supports logging, tracking, and tagging requests. All done through headers. Since this is context specific, we’ll pass in the context as an argument to the constructor:
1 def initialize(user)
2 @auth = {
3 username: ENV["API_USERNAME"],
4 password: ENV["API_PASSWORD"],
5 }
6
7 @headers = {
8 "Content-Type" => "application/json",
9 "X-User-ID" => user.tracking_id,
10 "X-Tags" => "Name:#{user.full_name}",
11 }
12 end
In addition to basic auth, we can also send in an OAuth style “Bearer” token:
1 @headers = {
2 "Content-Type" => "application/json",
3 "X-User-ID" => user.tracking_id,
4 "X-Tags" => "Name:#{user.full_name}",
5 "Authorisation" => "Bearer:#{user.oauth_token}",
6 }
Okay, this last example was lame. But the point remains, HTTParty allows you to build your service objects and gets out of your way. That is exactly what a library should do.
If you take a close look at our examples, consuming an HTTP API is all about:
Setting Headers
Specifying Query parameters
Request body encoding
Parsing responses based on the content type
Making the HTTP requests
Not surprising, right? HTTParty makes it simple and intuitive to perform all these actions. And hence it remains my favorite.
P.S. Here’s the code used above in a Gist.