Learning By Example: 4+1 Web APIs
We’re going to take a quick look at a few obscure web APIs and see if we can’t learn a thing or two about what to and not to do when designing web APIs.
My focus will be the REST (Representational state transfer) style of designing web APIs, though not all the APIs I’ll look at claim to be RESTful. I’m not going to give an introduction to REST here because there are already very good ones out there.
1. MyAnimeList
MyAnimeList is a website for tracking the anime you’ve watched and the manga you’ve read. From the website you can add new ones, update them, etc.. And that’s all supported via the API. So, this one’s not too bad at all. Though it’s not quite RESTful in it’s url structure.
For example, the supported urls and corresponding HTTP methods for your anime list are:
http://myanimelist.net/api/animelist/add/id.xml (POST) http://myanimelist.net/api/animelist/update/id.xml (POST) http://myanimelist.net/api/animelist/delete/id.xml (POST, DELETE)
With REST, you don’t want separate urls for each function. The url specifies the resource and the HTTP Method says what to do.
Rather, to add an anime it should be
http://myanimelist.net/api/animelist (POST)
to update it
http://myanimelist.net/api/animelist/id (PUT)
and to delete it
http://myanimelist.net/api/animelist/id (DELETE)
And while you can add, update and delete, there’s one glaring function missing – retrieving items from your list. You can query the entire database with
http://myanimelist.net/api/anime/search.xml?q=full+metal
but that wont give a user’s personal information regarding the results, such as number of episodes watched.
The only way I’ve found of getting a user’s list is undocumented, and not under /api/. It is:
http://myanimelist.net/malappinfo.php?u=docmarionum1&type=anime
That will some user info and their entire anime list (in this case my own). And this will get you the specific user’s information about the series, as well as the global data. This will pull their entire list and here is an example of one entry:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<anime>
<series_animedb_id>5081</series_animedb_id>
<series_title>Bakemonogatari</series_title>
<series_synonyms>Ghostory;Monstory</series_synonyms>
<series_type>1</series_type>
<series_episodes>15</series_episodes>
<series_status>2</series_status>
<series_start>2009-07-03</series_start>
<series_end>2010-06-25</series_end>
<series_image>http://cdn.myanimelist.net/images/anime/11/25771.jpg</series_image>
<my_id>9850932</my_id>
<my_watched_episodes>15</my_watched_episodes>
<my_start_date>2009-07-13</my_start_date>
<my_finish_date>2010-07-03</my_finish_date>
<my_score>10</my_score>
<my_status>2</my_status>
<my_rewatching/>
<my_rewatching_ep>0</my_rewatching_ep>
<my_last_updated>1278192197</my_last_updated>
<my_tags/>
</anime> |
So, not perfect, but it at least resembles a RESTful API.
Lesson Learned: RESTful URLs should be nouns which represent resources (i.e. animelist, pickle, cow) and the HTTP method used to access it defines the action. POST creates something new, PUT updates existing resources, GET just returns the information associated with a resource and DELETE…deletes.
2. Steam
For those who don’t know, Steam is a digital games distribution software. They offer an API for getting information about players.
A sample call looks like:
http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=XXXXXXXXXXX&steamids=76561197961911445
where key is your personal API key and steamids is a comma separated list of player ids. In this case, I queried my own account, and the response is:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
{
"response": {
"players": [
{
"steamid": "76561197961911445",
"communityvisibilitystate": 3,
"profilestate": 1,
"personaname": "docmarionum1",
"lastlogoff": 1328542282,
"profileurl": "http:\/\/steamcommunity.com\/id\/docmarionum1\/",
"avatar": "http:\/\/media.steampowered.com\/steamcommunity\/public\/images\/avatars\/03\/033af03cc7a5f9a9ca62ba8b9f9b30efbf260105.jpg",
"avatarmedium": "http:\/\/media.steampowered.com\/steamcommunity\/public\/images\/avatars\/03\/033af03cc7a5f9a9ca62ba8b9f9b30efbf260105_medium.jpg",
"avatarfull": "http:\/\/media.steampowered.com\/steamcommunity\/public\/images\/avatars\/03\/033af03cc7a5f9a9ca62ba8b9f9b30efbf260105_full.jpg",
"personastate": 1,
"primaryclanid": "103582791430108268",
"timecreated": 1065743681,
"loccountrycode": "US"
}
]
} |
The default format is JSON, but by passing a format parameter the format can also be changed to XML or a proprietary format called VDF (Valve Data Format).
The Steam API is not a great example of a web API. There are only 4 functions and they are not consistent. They also have long URLs, and all have three sub-urls, even though two could be uniquely identified by the first.
Then there’s the matter of the version number (v0002). Having a versioned API sounds like it could be a good idea so changes wont break old clients if you need to change formats or add new attributes, but that then requires your code to completely support all the old versions. One recommendation is to not include the version number in the URL, but also provide an alias to it with the version number, such as
http://api.steampowered.com/ISteamUser/GetPlayerSummaries/ http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/
both pointing to the same place while
http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0001/
will return a redirect code, indicating that the API has changed.
Lesson Learned: Only use version numbers to indicate to clients when they are using an obsolete version. Don’t make it a required part of the URL.
3. MTA
“Whiz kid certified” my ass. I wouldn’t even call this an API, more like a collection of somewhat-frequently updated information. Most of it is in zip format. Try using that in your dynamic application. Though there are two usable datasets. One is for the status of escalators and elevators throughout the system and the other has the status of subways, buses and trains. That’s really the most important one, so at least that’s available. Let’s take a look.
Hitting http://www.mta.info/status/serviceStatus.txt will give you an xml file with the time updated, and the status of all the lines.
A subway line block looks like:
|
0 1 2 3 4 5 6 7 8 |
<line>
<name>123</name>
<status>GOOD SERVICE</status>
<text></text>
<plannedworkheadline></plannedworkheadline>
<url></url>
<Date>02/08/2012</Date>
<Time> 6:12AM</Time>
</line> |
Looks servicable. And even more surprisingly, only one train line had any delays.
|
0 1 2 3 4 5 6 7 8 |
<line>
<name>NQR</name>
<status>SERVICE CHANGE</status>
<text><P>Following an earlier incident [N], [Q] and [R] train service has resumed with residual delays. </P></text>
<plannedworkheadline></plannedworkheadline>
<url></url>
<Date>02/08/2012</Date>
<Time> 8:53AM</Time>
</line> |
Fuck – that’s my line. Also notice that they’ve encoded html tags into the text field. That gibberish translates to <P> and </P>. Why would they put formatting information into there? The application using this should be responsible for doing any formatting necessary.
The offense is even more obvious when we take a look at one of the delayed bus lines.
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
<line>
<name>M1 - M116</name>
<status>SERVICE CHANGE</status>
<text>
<span class="TitleServiceChange" >Service Change</span>
<span class="DateStyle">
&nbsp;Posted:&nbsp;02/08/2012&nbsp; 6:32AM
</span>
<br/>
<P>Effective February 3, 2012 the M10 last stop and southern terminal are relocated from Broadway, far side W 58 St to Broadway near side W 57 St. </P>
<P>Additionally the first northbound M10 bus stop is relocated from W 57 St, near side of 8 Av, to far side W 57 St. These changes are made for operational improvement. </P>
<P><U><STRONG>BUS STOPS</STRONG></U></P>
<P><U><STRONG>New M10 Southbound Stop:</STRONG></U></P>
<UL>
<LI>On Broadway, near side, W 57 St</LI></UL>
<P><U><STRONG>New M10 Northbound Stop:</STRONG></U></P>
<UL>
<LI>On 8 Av, far side W 57 St</LI></UL>
<P><U><STRONG>Eliminated M10 Northbound Stop:</STRONG></U></P>
<UL>
<LI>On W 57 St, near side,8 Av</LI></UL>
<P><U><STRONG>Terminal Stand:</STRONG></U></P>
<UL>
<LI>On Broadway, near side West 57 St </LI></UL>
<br/>
<span class="TitleServiceChange" >Service Change</span>
<span class="DateStyle">
&nbsp;Posted:&nbsp;02/08/2012&nbsp; 5:01AM
</span>
<br/>
<P><STRONG>Q33</STRONG>, <STRONG>Q48</STRONG>, <STRONG>Q72</STRONG> and <STRONG>M60</STRONG> buses are detoured due to water main repairs at Laguardia Airport.</P>
<P><STRONG>Q33</STRONG> and <STRONG>Q72</STRONG> (Northbound), <STRONG>Q48</STRONG> (Westbound), <STRONG>M60</STRONG> (Southbound): Regular route into Airport, After Service the Delta and US Airways Terminal, exit airport at the 102nd St exit ramp, right on Ditmars Blvd, right on 94th St entrance to Central Terminal and regular route exiting airport> </P>
<P>Central Terminal Bus Stop Will be made at Parking Lot #1 bus stop. </P>
<P>Please allow additional travel time. </P>
<br/>
<span class="TitleServiceChange" >Service Change</span>
<span class="DateStyle">
&nbsp;Posted:&nbsp;02/08/2012&nbsp; 4:41AM
</span>
<br/>
<P><STRONG>M15</STRONG> buses are detoured and delayed, due to a traffic light outage on Delancy Street at Allen Street. </P>
<P>The detour is as follows:</P>
<P><STRONG>Southbound</STRONG>: Via Houston Street, right on Essex Street, right on Grand Street, left on Allen Street and then regular route.</P>
<P><STRONG>Northbound</STRONG>: Via Allen Street, right on Grand Street, left on Essex Street, left on Delancey Street, right on Allen Street and then regular route.</P>
<P>All buses will make all corresponding stops. </P>
<P>Please allow for additional travel time.</P>
<br/>
</text>
<Date>02/08/2012</Date>
<Time> 6:32AM</Time>
</line> |
What in the unholy fuck is that? This clearly contains styling information specific for the MTA’s service status page. Why would you leave that in a data source that is supposed to be used by the public? Anyone else who wants to use this first has to get rid of that crap.
Well, I can’t really say that I’m surprised with the state of the MTA’s “APIs.”
Lesson Learned: A web API should only return data, not formatting or styling information.
4. Biblia
Biblia’s API allows for quite a number of operations – listing different bibles, searching bibles, comparing verses, etc. It’s not RESTful, but it is full-featured.
I’m just going to use the standard King James Bible. We can first get some information about it with the find function
http://api.biblia.com/v1/bible/find.xml?key=XXXXXXXXXXXXXXXXX&bible=kjv
Which returns
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
{
"bibles":
[{
"bible":"kjv",
"title":"Authorized Version",
"abbreviatedTitle":"AV",
"publicationDate":"1995",
"languages":["en"],
"publishers":["Logos Research Systems, Inc."],
"imageUrl":"http://covers.logoscdn.com/lls_1.0.3/cover.jpg",
"description":"Also known as the \"Authorized Version,\" The King James Version of the Bible is still the most widely used text in the English language. The Logos KJV includes the Strong's Numbers which allow English readers to identify and search for underlying Greek and Hebrew words in the original text.",
"searchFields":["heading","largetext","surface","bible","words-of-christ","footnote"],
"copyright":"Public Domain",
"extendedCopyright":""
}]
} |
Then we can search the text of that bible with
http://api.biblia.com/v1/bible/search/kjv.js?query=ass&mode=verse&start=0&limit=3&key=XXXXXXXXXXX
The last part of the url specifies the bible to search, in this case kjv for the “Authorized Version” of the King James Bible. I’m querying for the first 3 instances of the word “ass.” The response is
|
0 1 2 3 4 5 6 7 8 9 10 11 |
{
"resultCount":87,
"hitCount":154,
"start":0,
"limit":3,
"results":
[
{"title":"Numbers 22:21","preview":"And Balaam rose up in the morning, and saddled his ass, and went with the princes of Moab."},
{"title":"1 Kings 13:13","preview":"And he said unto his sons, Saddle me the ass. So they saddled him the ass: and he rode thereon,"},
{"title":"Judges 19:3","preview":"And her husband arose, and went after her, to speak friendly unto her, and to bring her again, having his servant with him, and a couple of asses: and she brought him into her father’s house: and when the father of the damsel saw him, he rejoiced to meet him."}
]
} |
The response tells us the number of different passages with the word and the total number of times it occurs, and then returns the specified number of passages. “saddled his ass”…hehehe.
I quite like the Biblia API. It’s extensive, offers a bunch of different format options (JSON, XML, txt, HTML, HTML wrapped in JSON), and is consistent.
Lesson Learned: If you want people to use your API, it should do lots of stuff and support as many formats as possible to make it easier to use.
+1. This Blog
Thanks to the JSON API plugin for WordPress, even this blog has an API. You can get posts by id, name, tag, etc.; create posts, and submit comments.
For example, to get this post:
http://thisisnotthedomainnameiwanted.com/api/get_post/?post_id=8
It returns
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
{
"status":"ok",
"post":
{
"id":8,
"type":"post",
"slug":"learning-by-example-101-web-apis",
"url":"http:\/\/thisisnotthedomainnameiwanted.com\/learning-by-example-101-web-apis\/",
"status":"private","title":"Private: Learning By Example: 4+1 Web APIs",
"title_plain":"Private: Learning By Example: 4+1 Web APIs",
"content":"<p>We’re going to take a quick look at a few obscure web APIs and see if we can’t learn a thing or two...",
"excerpt":"...",
"date":"2012-02-07 04:52:39",
"modified":"2012-02-08 21:28:38",
"categories":[],
"tags":[],
"author":{"id":1,"slug":"admin","name":"admin","first_name":"","last_name":"","nickname":"admin","url":"","description":""},
"comments":[],
"attachments":[],
"comment_count":0,
"comment_status":"open"
}
} |
You can’t do everything through the API, but if you’re running a WordPress site, there’s no reason you shouldn’t have it.
Lesson Learned: You never know when someone might want to use your content. Make it as easy as possible to use.