api design - 3rd edition
DESCRIPTION
TRANSCRIPT
API Design3rd Edition
Kevin Swiber@kevinswiber
Apigee@apigee
Brian Mulloy@landlessness
groups.google.com/group/api-craft
youtube.com/apigee
slideshare.net/apigee
@landlessness @kevinswiber
“ The real issue is about design: designing things that have the power required for the job while maintaining understandability, the feeling of control, and the pleasure of accomplishment.
-Donald Norman
http://www.flickr.com/photos/mattharvey1/5712604622/
Agenda
• Recap Previous Edition• API Modeling• Security• Message Design• Hypermedia• Transactions
http://offers.apigee.com/web-api-design-ebook/
URL DesignPlural nouns for collections
/dogs
ID for entity /dogs/1234
Associations /owners/5678/dogs
4 HTTP Methods
POST GET PUT DELETE
Bias toward concrete names
/dogs (not animals)
Multiple formats in URL
/dogs.json/dogs.xml
Paginate with limit and offset
?limit=10&offset=0
Query params ?color=red&state=running
Partial selection ?fields=name,state
Use medial capitalization
"createdAt": 1320296464myObject.createdAt;
Use verbs for non-resource requests
/convert?from=EUR&to=CNY&amount=100
Search /search?q=happy%2Blabrador
DNS api.foo.comdevelopers.foo.com
Errors8 Status Codes 200 201 304 400 401 403 404
500
Verbose messages {"msg": "verbose, plain language hints"}
VersioningInclude version in URL
/v1/dogs
Keep one previous version long enough for developers to migrate
/v1/dogs/v2/dogs
Client ConsiderationsClient does not support HTTP status codes
?suppress_response_codes=true
Client does not support HTTP methods
GET /dogs?method=postGET /dogsGET /dogs?method=putGET /dogs?method=delete
Complement API with SDK and code libraries
1. JavaScript2. …3. …
How do we get started with our API?
Build an API Model
http://www.flickr.com/photos/brent_nashville/2156695472/in/photostream/
Don’t Go Cowboy
http://www.flickr.com/photos/theory/3364213389/in/photostream/
How do we secure our API?
Authorization: Basic aWhlYXJ0OmFwaXM=
Twitter Streaming API
Authorization: AWS AKIAIOSFODNN7EXAMPLE:frJIUNo//yllqDzg=
Amazon Web Services API
Authorization: Bearer 1/fFBGRNJru1FQd44AzqT3Zg
Google API
Authorization: Bearer 1/fFBGRNJru1FQd44AzqT3Zg
OAuth2
How do approach message design?
Support multiple formats JSON and XML
Make JSON the default
How do we represent single items?
21
Twitter Foursquare{ "meta": {…}, "data": {}}
Instagram{ "meta": {…}, "notifications": […], "response": {}}
{ "created_at": "Thu Jan 10 08:44:59 +0000 2013", "id": 289291736440791040, "id_str": "289291736440791040", "text": "@landlessness here's one for you: 50-year plan to fix Detroit\n\nhttp://t.co/kJ2l1FZv", "source": "<a href="http://twitter.com/download/android" rel="nofollow">Twitter for Android</a>", "truncated": false, "in_reply_to_status_id": null, "in_reply_to_status_id_str": null, "in_reply_to_user_id": 41020312, "in_reply_to_user_id_str": "41020312", "in_reply_to_screen_name": "landlessness", "user": {…}, "geo": {…}, "coordinates": {…}, "place": {…}, "contributors”:{…}, "retweet_count": 0, "entities": {…}, "favorited": false, "retweeted": false, "possibly_sensitive": false}
22
Twitter Foursquare{ "meta": {…}, "data": { "attribution": {…}, "type": "image", "location": {…}, "comments": {…}, "filter": "Sierra", "created_time": "1357826573", "link": "http://instagr.am/p/UTk5Xut3gN/", "likes": {…}, "images": {…}, "caption": {…}, "user_has_liked": false, "id": "365798266911553549_3573549", "user": {…} }}
Instagram{ "meta": {…}, "notifications": […], "response": { "checkin": { "id": "50eeff78e4b0f8e9624ea5f8", "createdAt": 1357840248, "type": "checkin", "shout": "Pharmacy #DRUGS!!! #ToothPulled :(", "timeZone": "America/Detroit", "timeZoneOffset": -300, "user": {…}, "venue": {…}, "source": {…} } }}
{ "created_at": "Thu Jan 10 08:44:59 +0000 2013", "id": 289291736440791040, "id_str": "289291736440791040", "text": "@landlessness here's one for you: 50-year plan to fix Detroit\n\nhttp://t.co/kJ2l1FZv", "source": "<a href="http://twitter.com/download/android" rel="nofollow">Twitter for Android</a>", "truncated": false, "in_reply_to_status_id": null, "in_reply_to_status_id_str": null, "in_reply_to_user_id": 41020312, "in_reply_to_user_id_str": "41020312", "in_reply_to_screen_name": "landlessness", "user": {…}, "geo": {…}, "coordinates": {…}, "place": {…}, "contributors”:{…}, "retweet_count": 0, "entities": {…}, "favorited": false, "retweeted": false, "possibly_sensitive": false}
23
Take the best of Foursquare and Instagram
{ "meta": {…}, "dog": {…} "notifications": […],}
How do we represent collections?
25
Twitter Foursquare{ "meta": {…}, "data": [ { "attribution": {…}, "type": "image", "location": {…}, "comments": {…}, "filter": "Sierra", "created_time": "1357826573", "link": "http://instagr.am/p/UTk5Xut3gN/", "likes": {…}, "images": {…}, "caption": {…}, "user_has_liked": false, "id": "365798266911553549_3573549", "user": {…} }, {…}, {…} ] }}
Instagram{ "meta": {…}, "notifications": […], "response": { "recent": [ { "id": "50eeff78e4b0f8e9624ea5f8", "createdAt": 1357840248, "type": "checkin", "shout": "Pharmacy #DRUGS!!! #ToothPulled :(", "timeZone": "America/Detroit", "timeZoneOffset": -300, "user": {…}, "venue": {…} }, {…}, {…}, ] }}
[ { "created_at": "Thu Jan 10 08:44:59 +0000 2013", "id": 289291736440791040, "id_str": "289291736440791040", "text": "@landlessness here's one for you: 50-year plan to fix Detroit\n\nhttp://t.co/kJ2l1FZv", "source": "<a href="http://twitter.com/download/android" rel="nofollow">Twitter for Android</a>", "truncated": false, "in_reply_to_status_id": null, "in_reply_to_status_id_str": null, "in_reply_to_user_id": 41020312, "in_reply_to_user_id_str": "41020312", "in_reply_to_screen_name": "landlessness", "user": {…}, "geo": {…}, "coordinates": {…}, "place": {…}, "contributors”:{…}, "retweet_count": 0, "entities": {…}, "favorited": false, "retweeted": false, "possibly_sensitive": false }, {…}, {…}]
26
Take the best of Foursquare and Instagram
{ "meta": {…}, "dogs": {…} /* include same info as single */ "notifications": […],}
How do we represent search results?
“ Selecting results is not the same as searching.
-Facebook API
29
Bing Search Google Custom Search{"kind": "Listing", "data": { "after": "t3_qy342", "before": null, "children": [ { "data": { "id": "f605o", "num_comments": 943, "score": 1146, "ups": 3110, "downs": 1964, "created": 1295553753.0, "url": "http://www.reddit.com/r/AskReddit/comments/f605o/this_is_a_long_shot_any_sushi_chefs_need_a_job_in/", "author": "jining", } }, { "data": { "id": "c9eng”, "num_comments": 308, "score": 59, "ups": 128, "downs": 69, "created": 1275155900.0, "url": "http://www.reddit.com/r/IAmA/comments/c9eng/i_am_a_sushi_man_ama/","saved": false, "is_self": true, "permalink": "/r/IAmA/comments/c9eng/i_am_a_sushi_man_ama/", "author": "IAmASushiMan” } } ] }}
Reddit Search{ "kind": "customsearch#search", "url": { "type": "application/json", "template": "https://www.googleapis.com/customsearch/v1?q={searchTerms}…}, "queries": { "request": [ { "title": "Google Custom Search - sushi", "totalResults": "15000000", "searchTerms": "sushi", "count": 10, "startIndex": 1, } ] }, "context": { "title": "Custom Search" }, "searchInformation": { "searchTime": 0.314942, "formattedSearchTime": "0.31", "totalResults": "15000000", "formattedTotalResults": "15,000,000" }, "items": [ { "kind": "customsearch#result", "title": "Standardized Usage Statistics Harvesting Initiative (SUSHI ... - NISO", "htmlTitle": "\u003cb\u003eStandardized Usage Statistics Harvesting Initiative\u003c/b\u003e (\u003cb\u003eSUSHI\u003c/b\u003e \u003cb\u003e...\u003c/b\u003e - NISO", "link": "http://www.niso.org/workrooms/sushi", "displayLink": "www.niso.org", "snippet": "The Standardized Usage Statistics Harvesting Initiative (SUSHI) Protocol standard (ANSI/NISO Z39.93-2007) defines an automated request and response model ...”,
{ "SearchResponse": { "Version": "2.2", "Query": { "SearchTerms": "sushi" }, "Web": { "Total": 95200000, "Offset": 0, "Results": [ { "Title": "The Sushi FAQ - The ultimate guide to sushi and sashimi and how to ...", "Description": "What is sushi?..", "Url": "http://www.sushifaq.com/", "CacheUrl": "http://cc.bingj.com/cache.aspx?q=sushi&d=4855190808495712&w=yU8fJS-YPT-f4svREMW2xSa75OoBUAZR", "DisplayUrl": "www.sushifaq.com", "DateTime": "2013-01-08T15:12:00Z" }, { "Title": "What Is Sushi? - Sushi Guide - Eatsushi.com", "Description": "Eatsushi.com...", "Url": "http://www.eatsushi.com/whatsushi.asp", "CacheUrl": "http://cc.bingj.com/cache.aspx?q=sushi&d=5013249854931333&w=ihBzI9k9WbrnwxKcV3n8mOoV97M89K-b", "DisplayUrl": "www.eatsushi.com/whatsushi.asp", "DateTime": "2013-01-07T13:51:00Z" } ] } }}
30
(Mostly) Follow Google Custom Search
{ "meta": { "limit": 1, "offset": 10, "totalResults": 15000000, "query": "sushi", "searchTime": 0.314942 }, "results": [ {}, {}, {} ]}
How do we represent links?
<link href=“http://api-public.netflix.com/catalog/people/100637” rel=“http://schemas.netflix.com/catalog/person.actor” title="Elijah Wood”></link>
Netflix API
"organization": { "login": "octocat", "id": 1, "url": "https://api.github.com/users/octocat", "type": "Organization”}
GitHub API
<link href=“http://api-public.netflix.com/catalog/people/100637” rel=“http://schemas.netflix.com/catalog/person.actor” title="Elijah Wood”></link>
Follow Netflix and the Web Linking spec
How do we represent actions?
GitHub
”actions": [{ “name”: “edit-repo”, “method”: “PATCH”, “href”: “https://api.github.com/repos/kevinswiber/siren”, ”fields”: [ { “name”: “name”, “type”: “text” }, { “name”: “description”, “type”: “text” }}]
Form-based API
"actions": [{ "name": "edit-repo", “method”: “PATCH”, “href”: “https://api.github.com/repos/kevinswiber/siren”, ”fields”: [ { “name”: “name”, “type”: “text” }, { “name”: “description”, “type”: “text” }}]
Form-based API
How do we represent metadata?
<item type="photo” id="10289” server="2” views="47” faves="0” more="0">
Flickr API (inline)
{ "size": "225.4KB”, "rev": "35e97029684fe”, "bytes": 230783, "modified": "Tue, 19 Jul 2011 21:55:38 +0000”, "path": "/Getting_Started.pdf”, "is_dir": false, "icon": "page_white_acrobat”, "root": "dropbox”, "mime_type": "application/pdf”, "revision": 220823}
Dropbox API (/metadata)
39
Include a metadata in responses and consider a dedicated/meta resource
{ "meta": { "size": "225.4KB”, "rev": "35e97029684fe”, "bytes": 230783, "modified": "Tue, 19 Jul 2011 21:55:38 +0000”, "path": "/Getting_Started.pdf”, "is_dir": false, "icon": "page_white_acrobat”, "root": "dropbox”, "mime_type": "application/pdf”, "revision": 220823 }}
What can we learn from hypermedia types?
Atom/AtomPub<?xml version="1.0"?><entry xmlns="http://www.w3.org/2005/Atom"> <title>My New Collection</title> <id>urn:uuid:de46e3a1-e489-41a6-88a6-21e7f0e8e2d8</id> <updated>2009-06-12T12:13:46Z</updated> <author> <name>Daffy</name> </author> <summary type="text" /> <content type="application/atom+xml;type=feed" src="http://example.org/my-new-collection"/> <link rel="edit” href="http://example.org/my-new-collection.atom" /></entry>
XHTML<ul class=“search user-list”> <li class=“user”> <div class="avatar"> <a href="/users/@kevin"> <img class=”user-image" src=”/img/avatar.png" /> </a> </div> <div> <a href=“/users/@kevin” rel=“user messages”> <span class=“user-name”>@kevin</span> (<span class="user-text">@kevin</span>) </a> </div> </li></ul>
HAL
{ “currentlyProcessing”: 14 “shippedToday”: 20, “_links”: { “self”: { “href”: “/orders?page=2” }, “next”: { “href”: “/orders?page=3” }, “prev”: { “href”: “/orders?page=1” } }}
Collection+JSON{ “collection”: { “version”: “1.0”, “href”: “http://example.org/friends”, “items”: [ “href”: “http://example.org/friends/kevin”, “data”: [ {“name”: “full-name”, “value”: “Kevin Swiber” } ] ], “queries”: [ {“rel”: “search”, “href”: “./search”, “data”: [ {“name”: “search”, “value”: “” } ] }}
Siren{ “class”: [“owner”, “vip”], “properties”: { “name”: “Kevin” }, “entities”: [ { “rel”: [“https://rels.x.io/dog”], “href”: “https://api.x.io/dogs/1” } ], “actions”: [ { “name”: “adopt”, “method”: “POST”, “href”: “https://api.x.io/owners/1/dogs”, “fields”: [ { “name”: “dog-name”, “type”: “text” } ] } ], “links”: [ { “rel”: [“self”], “href”: “https://api.x.io/owners/1” } ]}
How do we accept binary data?
multipart/form-data
Content-Type: multipart/form-data; boundary=AaB03x
--AaB03xContent-Disposition: form-data; name=“caption”
Cool picture of my cat.--AaB03xContent-Disposition: form-data; name=“photo”; filename=“catpajamas.jpg”Content-Type: image/jpegContent-Transfer-Encoding: binary
…contents of catpajamas.jpg…--AaB03x
Inline Base64 Encoding
POST /photos{ “caption”: “Cool picture of my cat.” “photo”: “RHVkZSwgbXkgY2F0IGhhcyB0aGUgYmVzdCBwYWphbWFzLg==”}
2-Step Process
POST /photos{ “caption”: “Cool picture of my cat.”}
PUT /photos/1234/dataContent-Type: image/jpegContent-Length: 240Content-Transfer-Encoding: binary
…binary content…
Opt for multipart/form-data.Be consistent.
How do we support caching?
Expiration
200 OKCache-Control: private, max-age=2592000
ETags
GET /dogs/1ETag: “a7D92kda94aisdfG”
GET /dogs/1If-None-Match: “a7D92kda94aisdfG”
Last-Modified
GET /dogs/1Last-Modified: Thu, 10 Jan 2013 19:43:31 GMT
GET /dogs/1If-Modified-Since: Thu, 10 Jan 2013 19:43:31 GMT
Think about the client.
Do we need a JavaScript API?
Yes. Follow LinkedIn’s lead.
What about posting data?
application/x-www-form-urlencoded
breed=Dachshund&name=Hotdog&age=2
application/xml
<dog> <breed>Dachshund</breed> <name>Hotdog</name> <age>2</age></dog>
application/json
{ “breed”: “Dachshund”, “name”: “Hotdog”, “age”: 2}
Favor application/x-www-form-urlencoded data.
How do we handle transactions?
Create a Transaction
POST /carts…201 CreatedLocation: /carts/1
Add ItemsPOST /carts/1/items/{ “productId”: “mittens123”, “quantity”: 1 }…201 CreatedLocation: /cartItems/1234
Commit the TransactionPOST /carts/1{ “message”: “checkout” }…200 OK
• Checkout previous editions for URI design• Start with API modeling• Use OAuth for security• Good message design is for developers• Learn from hypermedia specs• More on transactions later
Summary
Questions?
THANK YOUSubscribe to API webinars at:
youtube.com/apigee
THANK YOUQuestions and ideas to:
groups.google.com/group/api-craft