Slides from RailsClub 2012 in Moscow.
WYNN NETHERLANDUser ExperiencePatterns for APIs
View Slide
Howdy!
Wynn NetherlandMy name is
.com
I write API wrappers
UX Patterns for APIsand anti-patterns^
Support curlingPATTERN
curl -i https://api.github.com/users/defunktHTTP/1.1 200 OKServer: nginxDate: Wed, 12 Sep 2012 14:07:43 GMTContent-Type: application/json; charset=utf-8Connection: keep-aliveStatus: 200 OKContent-Length: 692X-Content-Type-Options: nosniffX-RateLimit-Remaining: 4997X-RateLimit-Limit: 5000Cache-Control: public, s-maxage=60, max-age=60Vary: AcceptX-GitHub-Media-Type: github.betaETag: "ef742caec0c19e2169ffb05e7d200d17"Last-Modified: Tue, 11 Sep 2012 02:52:21 GMT{"following": 212,"type": "User","html_url": "https://github.com/defunkt","location": "San Francisco","hireable": true,
curl -i -u pengwynn \https://api.github.com/user
curl -i -u pengwynn \https://api.github.com/userinclude response headers
curl -i -u pengwynn \https://api.github.com/useruse Basic Auth, prompt for password
Security mazesANTI-PATTERN
OAuth is painful.OAuth2 hurts less.
Insane URLsANTI-PATTERN
http://sandbox.api.shopping.com/publisher/3.0/rest/GeneralSearch?hybridSortType=relevance&productSortType=relevance&offerSortType=store-name&productReviewSortType=review-date
http://maps.googleapis.com/maps/api/staticmap?center=Brooklyn+Bridge,New+York,NY&zoom=13&size=600x300&maptype=roadmap&markers=color:blue%7Clabel:S%7C40.702147,-74.015794&markers=color:green%7Clabel:G%7C40.711614,-74.012318&markers=color:red%7Ccolor:red%7Clabel:C%7C40.718217,-73.998284&sensor=false
Maximize the payloadPATTERN
{"uri": "/pengwynn/githubbers","name": "GitHubbers","full_name": "@pengwynn/githubbers","description": "","mode": "public","user": {"id": 14100886,"favourites_count": 275,"profile_image_url": "http://a0.twimg.com/profile_ima...","profile_background_tile": false,"profile_sidebar_fill_color": "DDEEF6","verified": false,"location": "Denton, TX","utc_offset": -21600,"name": "Wynn Netherland","lang": "en","is_translator": false,"default_profile": true,"profile_background_color": "C0DEED","protected": false,"contributors_enabled": false,"time_zone": "Central Time (US & Canada)","profile_background_image_url": "http://a0.twimg.com/images/themes/...","profile_link_color": "0084B4","description": "Christian, husband, father, GitHubber, Co-host...","geo_enabled": true,"listed_count": 388,"show_all_inline_media": true,"notifications": false,"id_str": "14100886","statuses_count": 7178,"profile_image_url_https": "https://si0.twimg.com/profile_images/2221455972/
{"uri": "/pengwynn/githubbers","name": "GitHubbers","full_name": "@pengwynn/githubbers","description": "","mode": "public","user": {"id": 14100886,"favourites_count": 275,"profile_image_url": "http://a0.twimg.com/profile_ima...","profile_background_tile": false,"profile_sidebar_fill_color": "DDEEF6","verified": false,"location": "Denton, TX","utc_offset": -21600,"name": "Wynn Netherland","lang": "en","is_translator": false,"default_profile": true,"profile_background_color": "C0DEED","protected": false,"contributors_enabled": false,"time_zone": "Central Time (US & Canada)","profile_background_image_url": "http://a0.twimg.com/images/themes/...","profile_link_color": "0084B4","description": "Christian, husband, father, GitHubber, Co-host...","geo_enabled": true,"listed_count": 388,"show_all_inline_media": true,"notifications": false,"id_str": "14100886","statuses_count": 7178,"profile_image_url_https": "https://si0.twimg.com/profile_images/2221455972/Full User object
Rock the cachePATTERN
curl -I https://api.github.com/users/defunktHTTP/1.1 200 OKServer: nginxDate: Wed, 12 Sep 2012 14:07:43 GMTContent-Type: application/json; charset=utf-8Connection: keep-aliveStatus: 200 OKContent-Length: 692X-Content-Type-Options: nosniffX-RateLimit-Remaining: 4997X-RateLimit-Limit: 5000Cache-Control: public, s-maxage=60, max-age=60Vary: AcceptX-GitHub-Media-Type: github.betaETag: "ef742caec0c19e2169ffb05e7d200d17"Last-Modified: Tue, 11 Sep 2012 02:52:21 GMT
curl -I https://api.github.com/users/defunktHTTP/1.1 200 OKServer: nginxDate: Wed, 12 Sep 2012 14:07:43 GMTContent-Type: application/json; charset=utf-8Connection: keep-aliveStatus: 200 OKContent-Length: 692X-Content-Type-Options: nosniffX-RateLimit-Remaining: 4997X-RateLimit-Limit: 5000Cache-Control: public, s-maxage=60, max-age=60Vary: AcceptX-GitHub-Media-Type: github.betaETag: "ef742caec0c19e2169ffb05e7d200d17"Last-Modified: Tue, 11 Sep 2012 02:52:21 GMTCache policy
curl -I https://api.github.com/users/defunktHTTP/1.1 200 OKServer: nginxDate: Wed, 12 Sep 2012 14:07:43 GMTContent-Type: application/json; charset=utf-8Connection: keep-aliveStatus: 200 OKContent-Length: 692X-Content-Type-Options: nosniffX-RateLimit-Remaining: 4997X-RateLimit-Limit: 5000Cache-Control: public, s-maxage=60, max-age=60Vary: AcceptX-GitHub-Media-Type: github.betaETag: "ef742caec0c19e2169ffb05e7d200d17"Last-Modified: Tue, 11 Sep 2012 02:52:21 GMTFingerprint
curl -I \-H 'If-None-Match:"ef742caec0c19e2169ffb05e7d200d17" \https://api.github.com/users/defunktHTTP/1.1 304 Not ModifiedServer: nginxDate: Wed, 12 Sep 2012 15:51:39 GMTConnection: keep-aliveStatus: 304 Not ModifiedX-RateLimit-Limit: 5000X-Content-Type-Options: nosniffVary: AcceptETag: "ef742caec0c19e2169ffb05e7d200d17"X-RateLimit-Remaining: 4997Last-Modified: Wed, 12 Sep 2012 01:38:14 GMTCache-Control: public, s-maxage=60, max-age=60
curl -H 'If-None-Match:"ef742caec0c19e2169ffb05e7d200d17" \https://api.github.com/users/defunktHTTP/1.1 304 Not ModifiedServer: nginxDate: Wed, 12 Sep 2012 15:51:39 GMTConnection: keep-aliveStatus: 304 Not ModifiedX-RateLimit-Limit: 5000X-Content-Type-Options: nosniffVary: AcceptETag: "ef742caec0c19e2169ffb05e7d200d17"X-RateLimit-Remaining: 4997Last-Modified: Wed, 12 Sep 2012 01:38:14 GMTCache-Control: public, s-maxage=60, max-age=60
$ curl -i https://api.github.com/userHTTP/1.1 200 OKCache-Control: private, max-age=60ETag: "644b5b0155e6404a9cc4bd9d8b1ae730"Last-Modified: Thu, 05 Jul 2012 15:31:30 GMTStatus: 200 OKVary: Accept, Authorization, CookieX-RateLimit-Limit: 5000X-RateLimit-Remaining: 4996$ curl -i https://api.github.com/user -H "If-Modified-Since: Thu, 05 Jul 201215:31:30 GMT"HTTP/1.1 304 Not ModifiedCache-Control: private, max-age=60Last-Modified: Thu, 05 Jul 2012 15:31:30 GMTStatus: 304 Not ModifiedVary: Accept, Authorization, CookieX-RateLimit-Limit: 5000X-RateLimit-Remaining: 4996$ curl -i https://api.github.com/user -H 'If-None-Match:"644b5b0155e6404a9cc4bd9d8b1ae730"'HTTP/1.1 304 Not ModifiedCache-Control: private, max-age=60ETag: "644b5b0155e6404a9cc4bd9d8b1ae730"Last-Modified: Thu, 05 Jul 2012 15:31:30 GMTStatus: 304 Not ModifiedVary: Accept, Authorization, CookieX-RateLimit-Limit: 5000X-RateLimit-Remaining: 4996
Dogfood itPATTERN
Buildsomethingmeaningfulwith your API.
Buildsomethingmeaningfulwith your API.Janky
Buildsomethingmeaningfulwith your API.JankyHeaven
Buildsomethingmeaningfulwith your API.JankyHeavenMonitors
Buildsomethingmeaningfulwith your API.JankyTeamHeavenMonitors
Buildsomethingmeaningfulwith your API.JankyTeamHireHeavenMonitors
Buildsomethingmeaningfulwith your API.JankyTeamHireHeavenMonitorsThe Setup™
Buildsomethingmeaningfulwith your API.JankyTeamHireHeavenMonitorsThe Setup™Graph Store
How GitHub uses theGitHub API.
How GitHub uses theGitHub API.AuthN
How GitHub uses theGitHub API.AuthNAuthZ
How GitHub uses theGitHub API.AuthNAuthZMerging
How GitHub uses theGitHub API.AuthNAuthZMergingCommit Status
How GitHub uses theGitHub API.AuthNAuthZMergingCommit StatusGFM
Oh, and native apps.
Oh, and native apps.GitHub for Mac
Oh, and native apps.GitHub for MacGitHub for Windows
PATTERNUse HTTP status codes
HTTP/1.1 200 OKServer: nginxDate: Wed, 12 Sep 2012 14:07:43 GMTContent-Type: application/json; charset=utf-8Connection: keep-aliveStatus: 200 OKETag: "ef742caec0c19e2169ffb05e7d200d17"Last-Modified: Tue, 11 Sep 2012 02:52:21 GMT{"error_code": 1234,"error": "User not found"}
HTTP/1.1 404 Not FoundServer: nginxDate: Wed, 12 Sep 2012 14:07:43 GMTContent-Type: application/json; charset=utf-8Connection: keep-aliveStatus: 404 Not Found
Confusing header with bodyANTI-PATTERN
Harsh rate limitsANTI-PATTERN
$ curl -i https://api.github.com/users/whatever?client_id=xxxxxxxxxxxxxx&client_secret=yyyyyyyyyyyyyyyyyyyyyHTTP/1.1 200 OKStatus: 200 OKX-RateLimit-Limit: 12500X-RateLimit-Remaining: 11966
Announce breaking changesPATTERN
Make documentation obviousPATTERN
Create awesome guidesPATTERN
ConsolesPATTERN
ANTI-PATTERNEndless terms of service changes
InconsistencyANTI-PATTERN
POST /gists/:id/forkPOST /repos/:user/:repo/merges
Fast, transparentsupportPATTERN
Free samplesPATTERN
Mashup friendlyPATTERN
JSONP
$ curl https://api.github.com?callback=foofoo({"meta": {"status": 200,"X-RateLimit-Limit": "5000","X-RateLimit-Remaining": "4966","Link": [ // pagination headers andother links["https://api.github.com?page=2",{"rel": "next"}]]},"data": {// the data}})
CORS
$ curl -i https://api.github.com \-H "Origin: http://calendaraboutnothing.com"HTTP/1.1 302 FoundAccess-Control-Allow-Origin: http://calendaraboutnothing.comAccess-Control-Expose-Headers: Link, X-RateLimit-Limit, X-RateLimit-Remaining, X-OAuth-Scopes, X-Accepted-OAuth-ScopesAccess-Control-Allow-Credentials: true
$ curl -i https://api.github.com \-H "Origin: http://calendaraboutnothing.com"-X OPTIONSHTTP/1.1 204 No ContentAccess-Control-Allow-Origin: http://calendaraboutnothing.comAccess-Control-Allow-Headers: Authorization, X-Requested-WithAccess-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETEAccess-Control-Expose-Headers: Link, X-RateLimit-Limit, X-RateLimit-Remaining, X-OAuth-Scopes, X-Accepted-OAuth-ScopesAccess-Control-Max-Age: 86400Access-Control-Allow-Credentials: true
StreamingPATTERN
Persistent HTTP
Updates as they happen
Thanks!