Making Mobile Web Services that Don't Suck

Making Mobile Web Services that Don't Suck

(Presented DroidCon India, 29 November 2013, Bangalore)

One of the biggest benefits of the mobile app revolution is the ability to present information from the internet in new and exciting ways. Behind every app that relies on online data is an online service which, if implemented well, will make it super-easy to let your app access everything it needs. Unfortunately, not every web service does this well.

In this presentation, we’ll be looking into design principles for web APIs with a focus on making things not suck for mobile app developers. A particular focus will be on making services that are robust in the face of unreliable network access.

E4e9b67c5e7c596193395c5dedc4d7ee?s=128

Christopher Neugebauer

November 29, 2013
Tweet

Transcript

  1. 1.

    Mobile Web Services that Don’t Suck Christopher Neugebauer @chrisjrn http://chris.neugebauer.id.au

    chris@neugebauer.id.au How to make This is a talk about writing web based services with an emphasis on making things not suck for people writing Mobile apps. - Developing for Android since 2010. Android specialist at Asdeq Labs, previously Secret Lab. - Everything has had to interface with a web service through an API of some description - Sometimes this process can be easy, but frequently, APIs are designed without Mobile in mind, and this can make things a pain. - This talk is a series of requests, ideas, and thought experiments for making Web Service APIs better for mobile apps.
  2. 2.

    Your Infrastructure Hates You! And how to work around it

    Christopher Neugebauer @chrisjrn http://chris.neugebauer.id.au chris@neugebauer.id.au The take-away idea from this talk: Your infrastructure hates you. Mobile networks can’t cope with the level of usage that we put through them now. The techniques that I’ll be showing off in this talk will hopefully show you how to work around the ways that Mobile networks suck.
  3. 3.

    This Talk Requests, random thoughts and ideas Primarily, this talk

    is a list of things that API developers should include. And things that you should demand of your API designer. There are also some random thoughts that I think would be cool but I haven’t actually seen implemented; and some ideas that I’ve had. I don’t guarantee that anything here makes sense, but feel free to pick and choose ones that take your fancy.
  4. 5.

    Networks! So, allow me to set the stage. This talk

    is about networks. And to be fair, the networks that we as developers have dealt with in the past are pretty good. The quality of networks that we’ve dealt with have lulled us into a false sense of security, with false expectations.
  5. 6.

    As developers -- even mobile developers -- we probably have

    a very high-end workstation to do our development work on.
  6. 7.

    And these workstations are probably connected to a gigabit ethernet

    network, via a very short cable, to a highly-reliable broadband internet connection.
  7. 8.

    Or possibly via local wireless, to the same reliable internet

    connection. The problem is that we also run our mobile development simulators on these highly reliable connections.
  8. 9.

    Your development network is ideal. -- Our development networks are

    almost perfect. They rarely disappear, and when they’re alive, things like packet loss and connection dropping are non-existent. Web services are built and tested with these sorts of networks in mind.
  9. 10.

    Ideal mobile networks don’t exist. -- Actual mobile devices run

    on mobile networks. And there is no such thing as an ideal mobile network. The combination of unreliable networks and erratic users is a nightmare; because the avenues for failure are huge:
  10. 12.

    Users use their devices on public transport, and put themselves

    through blackspots like tunnels and crowded places like the city centre
  11. 15.

    But still, basically every app out there needs to do

    some sort of communication with a service on the cloud. And for this reason, our apps need to work with the infrastructure that we’ve provided.
  12. 17.

    “Cell phone tower” CC-BY-SA Joe Ravi So what I want

    to do in this talk is explain – properly – what we as Mobile developers need to understand about the networks that we’re working with. And with that, I’ll explain some API design ideas that you can take to your back-end team, so that your next network-bound app doesn’t have to suck.
  13. 18.

    1) Understand Your Network We’ll do this by first explaining

    a bit about networks and how they work.
  14. 19.

    2) Design for Failure Then we’ll look at ideas for

    APIs that work in the face of failure.
  15. 20.

    3) Design to be useful Following that, we’ll look at

    making APIs actually useful for mobile apps.
  16. 21.

    Make your API not suck At the end of this

    talk, you’ll hopefully know how to make an API that doesn’t suck.
  17. 22.

    Part 1 Know Your Network To design an API for

    mobile networks, you must first understand how mobile networks work. Mobile networks are inherently different to fixed networks for various reasons.
  18. 23.

    Fig 1. Typical GSM-era phone This is my first phone

    -- a Nokia 5110. It was released in the late 1990s, and was the most popular phone of its era. It connected to GSM mobile networks, which are the basis for every mobile network in Australia.
  19. 24.

    Make & Receive Calls Send & Receive SMS As far

    as I could tell, my 5110 had three important features: - It could be used as a telephone - It could be used to send/receive text messages - It’s killer feature was the first real example of an app -- the game “snake”. To support these features, a network would need to provide a minimum of features.
  20. 25.

    Make & Receive Calls • Low bandwidth (max 12.2 kbit/s

    each way) • Best-effort delivery You’d need 12.2kbit of voice transmission either way to get voice signals sent. And the transmission protocol is best-effort -- when transmitting voice, you can drop frames and still have your listener understand you. We’re also pretty good at repeating ourselves when a sentence fails to get transmitted.
  21. 26.

    Send & Receive SMS • Low bandwidth (max 160B +

    ack) • Best-effort delivery SMSes also require not a lot of bandwidth. And surprisingly, the delivery protocol for SMS is also best-effort (because, as twitter has proven, you can’t possibly transmit something important in 160 characters?)
  22. 27.

    Fig 2. Typical 1990s-era Internet At the same time, the

    internet on desktop machines was just starting to take off. In more advanced countries, the first residential cable modems were going in. Millions of 13-year-olds were discovering web design for the first time (thanks Geocities).
  23. 28.

    And the experience of the internet would be on a

    beige PC of some description. The experience of the internet wouldn’t change for quite some time -- we moved to laptops and wi-fi, but the way that the internet was consumed didn’t really change.
  24. 29.
  25. 30.

    Designing for internet Designing for mobile internet But there’s a

    problem. Our way of designing internet applications is antique, and comes with a fixed set of assumptions. <CLICK> The problem is that these assumptions have carried over to designing for Mobile devices too. All too often the same service is provided, with the app as a “skin”. This rarely works.
  26. 31.

    Ethernet / Dial-up / Cable Modem / Etc Internet Protocol

    Transmission Control Protocol Internet-enabled Application Wants Reliability Reliable Best-effort Very reliable Internet Application Stack So, let’s look at a traditional internet stack. At the centre is TCP and IP -- IP is a best-effort protocol, but TCP adds reliability guarantees, and our underlying networks just work. This provides a set of assumptions...
  27. 32.

    • Everything usually works • Errors are catastrophic • Failure

    is infrequent We expect our network to just work, and this is the usual case. We can expect our apps to be able to connect to a network. When an error occurs, this is usually due to a catastrophic failure -- network devices have broken, or a remote server is down. These sorts of failures are very infrequent.
  28. 33.

    Ethernet / Dial-up / Cable Modem / Etc Internet Protocol

    Transmission Control Protocol Internet-enabled Application Wants Reliability Reliable Best-effort Very reliable Internet Application Stack So what’s different about mobile networks?
  29. 34.

    Ethernet / Dial-up / Cable Modem / Etc Internet Protocol

    Transmission Control Protocol Internet-enabled Application Wants Reliability Reliable Best-effort Very reliable Internet Application Stack Well, on fixed networks, we have a very reliable network link layer.
  30. 35.

    3G Cellular Network Internet Protocol Transmission Control Protocol Internet-enabled Application

    Wants Reliability Reliable Best-effort Best-effort Mobile Application Stack On mobile networks, we have a best-effort link layer. Things don’t always work, and they *usually* don’t work. This creates an entirely different dynamic for network usage.
  31. 36.

    • Things can work (sometimes) • Errors are frequent (but

    rarely fatal) • Failure is frequent We can occasionally expect for things to work. The most frequent error case isn’t that a server is down, but that our link has failed; and these sorts of failures are part of day-to-day usage.
  32. 37.

    != So it’s clear that even though we may be

    doing the same things, mobile internet is not the same as internet on our desktop.
  33. 38.

    But users expect for them to be the same --

    our usage patterns don’t change, and we still expect to be able to do the same things with the same reliability. And given that our apps are the things that face the user, we’re the ones who get the blame when things don’t work.
  34. 40.

    Negotiate Connection Request Data Receive Response In an ideal world,

    we merely have to negotiate a connection from our network; then we can request data from our server, and the server gives a response back. <CLICK> In some protocols, the same connection can be re-used frequently. This is useful, as it saves on making extra new connections. (In HTTP, this is called Keep- Alive)
  35. 41.

    Negotiate Connection 1) Do a DNS lookup 2) (Repeat) 3)

    ... 4) Connect to server As it turns out, making a connection on the internet is surprisingly difficult -- frequently there are things like DNS lookups that need to be made (especially if you have replication).
  36. 42.

    Lots of sockets So, making one connection to a server

    can involve making LOTS of extraneous connections.
  37. 43.

    Sockets on 3G are expensive The problem is that requesting

    a socket on a 3G network is actually pretty slow compared with fixed networks.
  38. 44.

    Objective: Reduce number of new connections So ideally, we want

    to reduce the number of connections we create. You could make your connection long-lived. Long-polling is great!
  39. 45.

    Problem: The problem is that all too frequently we can

    lose all signal; so it’s a bad idea to expect any given connection to hang around for very long at all.
  40. 46.

    Objective: Keep connections short-lived So something else we’d like to

    do is to not rely on connections being up for a long time.
  41. 47.

    These two goals are in direct conflict. And so we

    now know that the life of a mobile designer is one of tradeoffs -- and designing an API that makes a mobile app happy is a matter of exploiting these tradeoffs.
  42. 48.

    Part 2 Plan to Fail The primary goal of a

    mobile API is to be able to get information from a remote service displayed coherently on a mobile device. The problem is that mobile networks don’t guarantee that data will reach you on first request, or sometimes at all. Planning to make APIs work for mobile is a matter of planning to fail.
  43. 49.

    HARD . . . . EASY You are here ––>

    In this section, I’m going to start off with the hard stuff, and get to stuff progressively easier as we go on... this is so you can remember the easy to fix stuff when you go off to fix your service.
  44. 50.

    Make everything static (Especially dynamic stuff) Serving static content is

    a solved problem. HTTP was designed with the intention of serving linked documents, and the design of HTTP was to ensure that things could be trivially cached in the case of faulty connections. The trick here is to make even dynamically-served things appear static.
  45. 51.

    Especially dynamic stuff So you may be thinking - “my

    web app generates content whenever you make a request! I can’t make everything static!” Well, in far too many cases, what you’re actually doing is serving data that is unlikely to change for a while. This is a waste.
  46. 52.

    (If data doesn’t change, then say so!) Important rule of

    thumb: if your app’s data doesn’t change, then say so! HTTP clients are designed to take advantage of static data.
  47. 53.

    HTTP/1.1 200 OK Date: Sun, 27 Oct 2013 06:57:43 GMT

    Server: Apache/2.2.22 (Debian) Last-Modified: Sun, 19 Sep 2010 01:09:58 GMT Expires: Sun, 05 Jan 2014 08:52:00 GMT ETag: "a015-0-4909274520580" Accept-Ranges: bytes Content-Length: 0 Vary: Accept-Encoding Content-Type: text/html This is a standard HTTP response. It contains a bunch of headers identifying various things about the response. Most important are the “Last-Modified” and “Expires” headers. If a last-modified header is present, a client can tell whether the data has changed since the last time it made that request (and not download it if needs be). The “ETag” header is a more machine-verifiable way of providing the Last-Modified information. If an Expires header is present, then the client can avoid having to re-download a request until after the expiry. This means that a particular call can function even if a device is offline. HTTP caches are pretty good at this.
  48. 54.

    Dynamic Data MORTAL ENEMY OF ALL CACHES The problem is

    that truly dynamic data is inherently uncacheable.
  49. 55.

    Make your responses reusable. The trick here is to ensure

    that once a request has been made, a response can be trivially replayed until it is no longer needed.
  50. 57.

    REST to ignorant people: http://api.foobar.com/users/chrisjrn NOT JUST PRETTY URLS Some

    people think that REST is just about making URLs that humans can read. You can do this without doing REST.
  51. 59.

    • Each resource has an identifier (a URI) • Resources

    never change • Make use of HTTP semantics
  52. 60.

    Use REST well So, if you use REST well, making

    resources reusable is easy. How do you do this?
  53. 61.

    Let URLs identify a specific response If you have a

    URL that points to a resource that frequently changes; make that URL generate a new URL that points to a specific response.
  54. 62.

    HTTP/1.1 307 Temporary Redirect Location: http://api.foobar.com/responses/ABC123 Have your app generate

    a response, and store it inside a temporary cache (Memcache or something like it), and have the use the cache key as an identifier. With HTTP Keep-Alive, making the second request to the URL of the response is basically free.
  55. 64.

    HTTP/1.1 206 Partial Content And this means that all of

    a sudden your “dynamic” responses are resumable!
  56. 65.

    • Clients can resume partially finished response • Clients do

    not need to completely restart requests that have failed
  57. 66.

    Resumable requests are VERY useful* (*Clients don’t always use these.

    Tell them about it if you support them) If you do make your API work with 206 headers, then make sure you document this -- it’s trivial to complete at a client end, but nobody ever bothers because APIs rarely support it.
  58. 67.

    Make Everything Repeatable If your API is highly dynamic, then

    it is very important to let your requests be repeatable -- it is important that a mobile app and the service be able to remain synchronised, and it’s far too easy to implement a service that gets ahead of itself.
  59. 68.

    Event-driven APIs The particular use case I see here is

    for event-driven APIs -- e.g. things that track movement on a map; messaging applications; everything where events change over time.
  60. 69.

    GET /events [{‘event_id’ : 1}, {‘event_id’ : 2}, {‘event_id’ :

    3}, {‘event_id’ : 4}, {‘event_id’ : 5}] GET /events [{‘event_id’ : 6}, {‘event_id’ : 7}, {‘event_id’ : 8}, {‘event_id’ : 9}, {‘event_id’ : 10}] GET /events [{‘event_id’ : 11}, {‘event_id’ : 12}, {‘event_id’ : 13}, {‘event_id’ : 14}, {‘event_id’ : 15}] The case I see is this: You make a URL called /events, which serves up the latest set of events that the app hasn’t received. Calling it each time sends a new set of events. The problem? What if your connection crashes halfway through a request -- suddenly, your app doesn’t know about events 6-10. It’s all to easy to write a PHP app which brazenly updates a database even if a response isn’t received correctly.
  61. 70.

    Only the app knows what state it’s in. In the

    mobile world, the only thing that knows what data it has and has not received is the mobile app itself. Servers can only make an educated guess. It is important to exploit this fact.
  62. 71.

    Inconsistency If you don’t, you just end up with inconsistency,

    and if your app shows inconsistent data, it’s completely not worth using.
  63. 72.

    GET /events?since=0 [{‘event_id’ : 1}, {‘event_id’ : 2}, {‘event_id’ :

    3}, {‘event_id’ : 4}, {‘event_id’ : 5}] GET /events?since=5 [{‘event_id’ : 6}, {‘event_id’ : 7}, {‘event_id’ : 8}, {‘event_id’ : 9}, {‘event_id’ : 10}] GET /events?since=5 [{‘event_id’ : 6}, {‘event_id’ : 7}, {‘event_id’ : 8}, {‘event_id’ : 9}, {‘event_id’ : 10}, {‘event_id’ : 11}, ... ] So one trick is to make sure that your app can repeatedly serve up events that it thinks it has already served (you can use memcache or something like that if you like throwing things away); you do this by adding a ‘since’ parameter -- so a client can ask for old events if it wants them. Suddenly, you can ensure consistency!
  64. 73.

    Bonus Points: Use partial data! For bonus, bandwidth-saving points, you

    can use partial data! You can do this by caching your responses like I showed before, or you can present your responses in a way that is amenable to incremental parsing.
  65. 75.

    XML JSON In the beginning, there was XML. It’s big

    and bloated and not particularly good to read, and it got used in far too many places. <CLICK> More recently, JSON came around, and replaces XML in a large number of places.
  66. 76.

    • Lightweight • Easy for humans to read • Maps

    well to standard data structures There’s good reasons for this -- JSON is awesome.
  67. 79.

    [ ..., ..., ..., ] 0 1 2 JSON has

    two ways of expressing compound data. You have lists, which store ordered information. <CLICK> the order in lists is quite important -- you can derive useful information from where something appears in a list.
  68. 80.

    { "..." : "...", "..." : "...", "..." : "...",

    } Then you have objects. These store unordered bits of information.
  69. 81.

    A B C D E F Here’s a reasonably standard

    data structure -- it’s a bunch of nested data structures of varying formats.
  70. 82.

    JSON Representation A B C D E F { "a"

    : { "b" : { "e" : {}, "f" : {}, }, "c" : {}, "d" : {} } In JSON, you can represent this tree as a set of nested objects -- this produces the same representation as I showed earlier.
  71. 83.

    The problem { "a" : { "b" : { "e"

    : {}, "f" : {}, }, "c" : {}, "d" : {} } The problem with JSON objects, however, is that objects don’t hold order. So let’s turn this into something a bit more concrete...
  72. 84.

    The problem { "user" : { "roles" : { "admin"

    : true, "contrib" : false, }, "twitter" : "chrisjrn", "name" : "chris jrn" } The problem with JSON objects, however, is that JSON *objects* don’t hold order. And that means that this object here, which represents our tree...
  73. 85.

    The problem { "user" : { "roles" : { "admin"

    : true, "contrib" : false, }, "name" : "chris jrn", "twitter" : "chrisjrn" } ... is semantically equivalent to this object.
  74. 86.

    Incremental Parsing With that in mind, we can look at

    one useful thing to do with incomplete data... That’s to be able to parse things incrementally -- this lets you show incomplete data whilst you wait for the remainder to load. Web browsers can do this to great effect -- if you abort loading a long web page, it’ll try to make sense of the result, and show you partial content. The problem is that not every format is amenable.
  75. 87.

    { "user" : { "roles" : { "admin" : true,

    "contrib" : false, }, "twitter" : "chrisjrn", "name" : "chris jrn" } If you have a complete JSON object that looks like this ...
  76. 88.

    { "user" : { "roles" : { "admin" : true,

    "contrib" : false, }, M EANINGLESS ... but only this much is loaded, you don’t have a valid JSON object. For a JSON object to make sense, you need to have every key present.
  77. 89.

    { "response_type" : "users" "users" : [ {"id" : 0,

    "name" : "Bill"}, {"id" : 1, "name" : "Anthony"}, {"id" : 2, "name" : "Julia"}, {"id" : 3, "name" : "Nicola"}, {"id" : 4, "name" : "Kevin"}, {"id" : 5, "name" : "Tony"} ], "response_id" : 5594 } Something I see commonly is to wrap a list of items inside a JSON object, which carries some extra information...
  78. 90.

    { "response_type" : "users" "users" : [ {"id" : 0,

    "name" : "Bill"}, {"id" : 1, "name" : "Anthony"}, {"id" : 2, "name" : "Julia"}, {"id" : 3, "na START AGAIN Well, if your response terminates early, then it’s impossible to stub out the object and hope that you’ve got some valid data. You need all of the keys there to parse it.
  79. 91.

    Record-oriented data So, if the most important part of your

    response is a list of events, or people, or anything you can make a list out of, what you have is record-oriented data. If you have truly record-oriented data, then use a record-driven format. Record- driven formats are trivial to parse incrementally, and are easy to conceptualise.
  80. 94.

    • Fixed-length records • Order is essential • Complete row

    -> complete record 1, "foo", 97, "baz" CSV has several key attributes: - Every record has the same number of fields, and every field appears in the same column of each row. This means that the format of your records/objects is rigid. - It also has a defined rule that tells you when you’ve got a complete record: you just look for a newline. Everything before a newline is valid data.
  81. 95.

    GET /events?since=0 1,”foo”,97,”baz” 2,”bar”,56,”quz” 3,”joe”,43,”bob” 4,”WOO”,81,”not” 5,”rea”,22,”quz” GET /events?since=5 6,”foo”,97,”baz”

    7,”bar”,56,”quz” 8,”why”,56,”are” 9,”you”,56,”rea” 10,”din”,56,”gth” GET /events?since=7 8,”why”,56,”are” 9,”you”,56,”rea” 10,”din”,56,”gth” 11,”is?”,99,”bob” 12,”did”,12,”not” 13,”bot”,13,”her” So, in an event-driven situation, using record-oriented data means that you can do something like this!
  82. 96.

    Don’t use CSV Just be mindful of it. Though, I

    actually don’t recommend using CSV at all as a format. It’s not very expressive -- you can tell easily which column corresponds to which data; it’s also hard in most languages to automatically parse CSV and produce useful objects to use. Instead, what you should do is think about whether you can use CSV to represent your data.
  83. 97.

    "id", "name" 0, "Bill" 1, "Anthony" 2, "Julia" 3, "Nicola"

    4, "Kevin" 5, "Tony" GET /people?from=0 HTTP/1.1 A good way to test whether you’ve got a record-oriented format is to figure out whether you can map your objects down to CSV. That means you have a fixed number of fields for each object, and every object is represented as a single CSV record So this CSV file...
  84. 98.

    [ {"id" : 0, "name" : "Bill"}, {"id" : 1,

    "name" : "Anthony"}, {"id" : 2, "name" : "Julia"}, {"id" : 3, "name" : "Nicola"}, {"id" : 4, "name" : "Kevin"}, {"id" : 5, "name" : "Tony"} ] GET /people?from=0 HTTP/1.1 ... maps down to a *list* of JSON objects -- lists are good because they guarantee order. each record is a JSON object with keys -- so it’s obviously semantic. But importantly, because you have a guaranteed order...
  85. 99.

    [ {"id" : 0, "name" : "Bill"}, {"id" : 1,

    "name" : "Anthony"}, {"id" : 2, "name" : "Julia"}, {"id" : 3, "na GET /people?from=0 HTTP/1.1 If your connection fails halfway through the response, it’s possible to figure out what you can salvage. In this case, we know that everything up to line two is there in order...
  86. 100.

    [ {"id" : 0, "name" : "Bill"}, {"id" : 1,

    "name" : "Anthony"}, {"id" : 2, "name" : "Julia"} ] so you can drop the incomplete objects, terminate the list, parse what you have, and display it on screen.
  87. 101.

    [ {"id" : 3, "name" : "Nicola"}, {"id" : 4,

    "name" : "Kevin"}, {"id" : 5, "name" : "Tony"} ] GET /people?from=3 HTTP/1.1 Then, you can go off and request the remainder of the object.
  88. 102.

    Keep Responses Small One easy way to make sure that

    your responses get to your app is to keep things as small as possible.
  89. 103.

    Counter-example: And to demonstrate the value of keeping things small,

    I’m going to show you how not to do this. The counter-example I’m showing is Twitter.
  90. 104.

    { "coordinates": null, "created_at": "Sat Sep 10 22:23:38 +0000 2011",

    "truncated": false, "favorited": false, "id_str": "112652479837110273", "entities": { "urls": [ { "expanded_url": "http://instagr.am/p/MuW67/", "url": "http://t.co/6J2EgYM", "indices": [ 67, 86 ], "display_url": "instagr.am/p/MuW67/" } ], "hashtags": [ { "text": "tcdisrupt", "indices": [ 32, 42 ] } ], "user_mentions": [ { "name": "Twitter", "id_str": "783214", "id": 783214, "indices": [ 0, 8 ], "screen_name": "twitter" }, { "name": "Picture.ly", "id_str": "334715534", "id": 334715534, "indices": [ 15, 28 ], "screen_name": "SeePicturely" }, { "name": "Bosco So", "id_str": "14792670", "id": 14792670, "indices": [ 46, 58 ], "screen_name": "boscomonkey" }, { "name": "Taylor Singletary", "id_str": "819797", "id": 819797, "indices": [ 59, 66 ], "screen_name": "episod" } ] }, "in_reply_to_user_id_str": "783214", "text": "@twitter meets @seepicturely at #tcdisrupt cc.@boscomonkey @episod http://t.co/6J2EgYM", "contributors": null, "id": 112652479837110273, "retweet_count": 0, "in_reply_to_status_id_str": null, "geo": null, "retweeted": false, "possibly_sensitive": false, "in_reply_to_user_id": 783214, "place": null, "source": "<a href=\"http://instagr.am\" rel= \"nofollow\">Instagram</a>", "user": { "profile_sidebar_border_color": "eeeeee", "profile_background_tile": true, "profile_sidebar_fill_color": "efefef", "name": "Eoin McMillan ", "profile_image_url": "http://a1.twimg.com/ profile_images/1380912173/ Screen_shot_2011-06-03_at_7.35.36_PM_normal.png", "created_at": "Mon May 16 20:07:59 +0000 2011", "location": "Twitter", "profile_link_color": "009999", "follow_request_sent": null, "is_translator": false, "id_str": "299862462", "favourites_count": 0, "default_profile": false, "url": "http://www.eoin.me", "contributors_enabled": false, "id": 299862462, "utc_offset": null, "profile_image_url_https": "https://si0.twimg.com/ profile_images/1380912173/ Screen_shot_2011-06-03_at_7.35.36_PM_normal.png", "profile_use_background_image": true, "listed_count": 0, "followers_count": 9, "lang": "en", "profile_text_color": "333333", "protected": false, "profile_background_image_url_https": "https:// si0.twimg.com/images/themes/theme14/bg.gif", "description": "Eoin's photography account. See @mceoin for tweets.", "geo_enabled": false, "verified": false, "profile_background_color": "131516", "time_zone": null, "notifications": null, "statuses_count": 255, "friends_count": 0, "default_profile_image": false, "profile_background_image_url": "http:// a1.twimg.com/images/themes/theme14/bg.gif", "screen_name": "imeoin", "following": null, "show_all_inline_media": false }, "in_reply_to_screen_name": "twitter", "in_reply_to_status_id": null } GET http://api.twitter.com/1/statuses/show.json?id=112652479837110273&include_entities=true What would you expect to receive when you request a tweet from Twitter? This is the JSON that gets returned.
  91. 105.

    { "coordinates": null, "created_at": "Sat Sep 10 22:23:38 +0000 2011",

    "truncated": false, "favorited": false, "id_str": "112652479837110273", "entities": { "urls": [ { "expanded_url": "http://instagr.am/p/MuW67/", "url": "http://t.co/6J2EgYM", "indices": [ 67, 86 ], "display_url": "instagr.am/p/MuW67/" } ], "hashtags": [ { "text": "tcdisrupt", "indices": [ 32, 42 ] } ], "user_mentions": [ { "name": "Twitter", "id_str": "783214", "id": 783214, "indices": [ 0, 8 ], "screen_name": "twitter" }, { "name": "Picture.ly", "id_str": "334715534", "id": 334715534, "indices": [ 15, 28 ], "screen_name": "SeePicturely" }, { "name": "Bosco So", "id_str": "14792670", "id": 14792670, "indices": [ 46, 58 ], "screen_name": "boscomonkey" }, { "name": "Taylor Singletary", "id_str": "819797", "id": 819797, "indices": [ 59, 66 ], "screen_name": "episod" } ] }, "in_reply_to_user_id_str": "783214", "text": "@twitter meets @seepicturely at #tcdisrupt cc.@boscomonkey @episod http://t.co/6J2EgYM", "contributors": null, "id": 112652479837110273, "retweet_count": 0, "in_reply_to_status_id_str": null, "geo": null, "retweeted": false, "possibly_sensitive": false, "in_reply_to_user_id": 783214, "place": null, "source": "<a href=\"http://instagr.am\" rel= \"nofollow\">Instagram</a>", "user": { "profile_sidebar_border_color": "eeeeee", "profile_background_tile": true, "profile_sidebar_fill_color": "efefef", "name": "Eoin McMillan ", "profile_image_url": "http://a1.twimg.com/ profile_images/1380912173/ Screen_shot_2011-06-03_at_7.35.36_PM_normal.png", "created_at": "Mon May 16 20:07:59 +0000 2011", "location": "Twitter", "profile_link_color": "009999", "follow_request_sent": null, "is_translator": false, "id_str": "299862462", "favourites_count": 0, "default_profile": false, "url": "http://www.eoin.me", "contributors_enabled": false, "id": 299862462, "utc_offset": null, "profile_image_url_https": "https://si0.twimg.com/ profile_images/1380912173/ Screen_shot_2011-06-03_at_7.35.36_PM_normal.png", "profile_use_background_image": true, "listed_count": 0, "followers_count": 9, "lang": "en", "profile_text_color": "333333", "protected": false, "profile_background_image_url_https": "https:// si0.twimg.com/images/themes/theme14/bg.gif", "description": "Eoin's photography account. See @mceoin for tweets.", "geo_enabled": false, "verified": false, "profile_background_color": "131516", "time_zone": null, "notifications": null, "statuses_count": 255, "friends_count": 0, "default_profile_image": false, "profile_background_image_url": "http:// a1.twimg.com/images/themes/theme14/bg.gif", "screen_name": "imeoin", "following": null, "show_all_inline_media": false }, "in_reply_to_screen_name": "twitter", "in_reply_to_status_id": null } GET http://api.twitter.com/1/statuses/show.json?id=112652479837110273&include_entities=true There in yellow is the actual contents of the tweet.
  92. 107.

    2425% overhead! For a 140-byte message, there’s over 24 times

    extra information delivered along with the request. This is just a bit bloated. So, let’s see what’s actually in that message.
  93. 108.

    { "coordinates": null, "created_at": "Sat Sep 10 22:23:38 +0000 2011",

    "truncated": false, "favorited": false, "id_str": "112652479837110273", "entities": { "urls": [ { "expanded_url": "http://instagr.am/p/MuW67/", "url": "http://t.co/6J2EgYM", "indices": [ 67, 86 ], "display_url": "instagr.am/p/MuW67/" } ], "hashtags": [ { "text": "tcdisrupt", "indices": [ 32, 42 ] } ], "user_mentions": [ { "name": "Twitter", "id_str": "783214", "id": 783214, "indices": [ 0, 8 ], "screen_name": "twitter" }, { "name": "Picture.ly", "id_str": "334715534", "id": 334715534, "indices": [ 15, 28 ], "screen_name": "SeePicturely" }, { "name": "Bosco So", "id_str": "14792670", "id": 14792670, "indices": [ 46, 58 ], "screen_name": "boscomonkey" }, { "name": "Taylor Singletary", "id_str": "819797", "id": 819797, "indices": [ 59, 66 ], "screen_name": "episod" } ] }, "in_reply_to_user_id_str": "783214", "text": "@twitter meets @seepicturely at #tcdisrupt cc.@boscomonkey @episod http://t.co/6J2EgYM", "contributors": null, "id": 112652479837110273, "retweet_count": 0, "in_reply_to_status_id_str": null, "geo": null, "retweeted": false, "possibly_sensitive": false, "in_reply_to_user_id": 783214, "place": null, "source": "<a href=\"http://instagr.am\" rel= \"nofollow\">Instagram</a>", "user": { "profile_sidebar_border_color": "eeeeee", "profile_background_tile": true, "profile_sidebar_fill_color": "efefef", "name": "Eoin McMillan ", "profile_image_url": "http://a1.twimg.com/ profile_images/1380912173/ Screen_shot_2011-06-03_at_7.35.36_PM_normal.png", "created_at": "Mon May 16 20:07:59 +0000 2011", "location": "Twitter", "profile_link_color": "009999", "follow_request_sent": null, "is_translator": false, "id_str": "299862462", "favourites_count": 0, "default_profile": false, "url": "http://www.eoin.me", "contributors_enabled": false, "id": 299862462, "utc_offset": null, "profile_image_url_https": "https://si0.twimg.com/ profile_images/1380912173/ Screen_shot_2011-06-03_at_7.35.36_PM_normal.png", "profile_use_background_image": true, "listed_count": 0, "followers_count": 9, "lang": "en", "profile_text_color": "333333", "protected": false, "profile_background_image_url_https": "https:// si0.twimg.com/images/themes/theme14/bg.gif", "description": "Eoin's photography account. See @mceoin for tweets.", "geo_enabled": false, "verified": false, "profile_background_color": "131516", "time_zone": null, "notifications": null, "statuses_count": 255, "friends_count": 0, "default_profile_image": false, "profile_background_image_url": "http:// a1.twimg.com/images/themes/theme14/bg.gif", "screen_name": "imeoin", "following": null, "show_all_inline_media": false }, "in_reply_to_screen_name": "twitter", "in_reply_to_status_id": null } GET http://api.twitter.com/1/statuses/show.json?id=112652479837110273&include_entities=true User profile -- 1540B Here we have 1540B worth of user profile information (all of the information required to replicate the user’s profile -- stuff that the twitter website would find useful, but most clients wouldn’t).
  94. 109.
  95. 110.

    PLEASE DON’T BE THIS BAD Please don’t be as bad

    as twitter here -- keep the amount of data you send to a minimum.
  96. 111.

    Minimise Requests Made The tradeoff here is that the more

    requests you need to make of an API, the more likely it is that one will fail.
  97. 112.

    LAG! The most noticable latency comes when initiating a request.

    Also, on mobile networks, initiating a connection is probably the highest source of latency. Therefore, it’s important to keep the number of connections to a minimum.
  98. 115.

    { "coordinates": null, "created_at": "Sat Sep 10 22:23:38 +0000 2011",

    "truncated": false, "favorited": false, "id_str": "112652479837110273", "entities": { "urls": [ { "expanded_url": "http://instagr.am/p/MuW67/", "url": "http://t.co/6J2EgYM", "indices": [ 67, 86 ], "display_url": "instagr.am/p/MuW67/" } ], "hashtags": [ { "text": "tcdisrupt", "indices": [ 32, 42 ] } ], "user_mentions": [ { "name": "Twitter", "id_str": "783214", "id": 783214, "indices": [ 0, 8 ], "screen_name": "twitter" }, { "name": "Picture.ly", "id_str": "334715534", "id": 334715534, "indices": [ 15, 28 ], "screen_name": "SeePicturely" }, { "name": "Bosco So", "id_str": "14792670", "id": 14792670, "indices": [ 46, 58 ], "screen_name": "boscomonkey" }, { "name": "Taylor Singletary", "id_str": "819797", "id": 819797, "indices": [ 59, 66 ], "screen_name": "episod" } ] }, "in_reply_to_user_id_str": "783214", "text": "@twitter meets @seepicturely at #tcdisrupt cc.@boscomonkey @episod http://t.co/6J2EgYM", "contributors": null, "id": 112652479837110273, "retweet_count": 0, "in_reply_to_status_id_str": null, "geo": null, "retweeted": false, "possibly_sensitive": false, "in_reply_to_user_id": 783214, "place": null, "source": "<a href=\"http://instagr.am\" rel= \"nofollow\">Instagram</a>", "user": { "profile_sidebar_border_color": "eeeeee", "profile_background_tile": true, "profile_sidebar_fill_color": "efefef", "name": "Eoin McMillan ", "profile_image_url": "http://a1.twimg.com/ profile_images/1380912173/ Screen_shot_2011-06-03_at_7.35.36_PM_normal.png", "created_at": "Mon May 16 20:07:59 +0000 2011", "location": "Twitter", "profile_link_color": "009999", "follow_request_sent": null, "is_translator": false, "id_str": "299862462", "favourites_count": 0, "default_profile": false, "url": "http://www.eoin.me", "contributors_enabled": false, "id": 299862462, "utc_offset": null, "profile_image_url_https": "https://si0.twimg.com/ profile_images/1380912173/ Screen_shot_2011-06-03_at_7.35.36_PM_normal.png", "profile_use_background_image": true, "listed_count": 0, "followers_count": 9, "lang": "en", "profile_text_color": "333333", "protected": false, "profile_background_image_url_https": "https:// si0.twimg.com/images/themes/theme14/bg.gif", "description": "Eoin's photography account. See @mceoin for tweets.", "geo_enabled": false, "verified": false, "profile_background_color": "131516", "time_zone": null, "notifications": null, "statuses_count": 255, "friends_count": 0, "default_profile_image": false, "profile_background_image_url": "http:// a1.twimg.com/images/themes/theme14/bg.gif", "screen_name": "imeoin", "following": null, "show_all_inline_media": false }, "in_reply_to_screen_name": "twitter", "in_reply_to_status_id": null } GET http://api.twitter.com/1/statuses/show.json?id=112652479837110273&include_entities=true Here’s our 3.3Kchar JSON dump, it contains the information that we want to display.
  99. 116.

    { "coordinates": null, "created_at": "Sat Sep 10 22:23:38 +0000 2011",

    "truncated": false, "favorited": false, "id_str": "112652479837110273", "entities": { "urls": [ { "expanded_url": "http://instagr.am/p/MuW67/", "url": "http://t.co/6J2EgYM", "indices": [ 67, 86 ], "display_url": "instagr.am/p/MuW67/" } ], "hashtags": [ { "text": "tcdisrupt", "indices": [ 32, 42 ] } ], "user_mentions": [ { "name": "Twitter", "id_str": "783214", "id": 783214, "indices": [ 0, 8 ], "screen_name": "twitter" }, { "name": "Picture.ly", "id_str": "334715534", "id": 334715534, "indices": [ 15, 28 ], "screen_name": "SeePicturely" }, { "name": "Bosco So", "id_str": "14792670", "id": 14792670, "indices": [ 46, 58 ], "screen_name": "boscomonkey" }, { "name": "Taylor Singletary", "id_str": "819797", "id": 819797, "indices": [ 59, 66 ], "screen_name": "episod" } ] }, "in_reply_to_user_id_str": "783214", "text": "@twitter meets @seepicturely at #tcdisrupt cc.@boscomonkey @episod http://t.co/6J2EgYM", "contributors": null, "id": 112652479837110273, "retweet_count": 0, "in_reply_to_status_id_str": null, "geo": null, "retweeted": false, "possibly_sensitive": false, "in_reply_to_user_id": 783214, "place": null, "source": "<a href=\"http://instagr.am\" rel= \"nofollow\">Instagram</a>", "user": { "profile_sidebar_border_color": "eeeeee", "profile_background_tile": true, "profile_sidebar_fill_color": "efefef", "name": "Eoin McMillan ", "profile_image_url": "http://a1.twimg.com/ profile_images/1380912173/ Screen_shot_2011-06-03_at_7.35.36_PM_normal.png", "created_at": "Mon May 16 20:07:59 +0000 2011", "location": "Twitter", "profile_link_color": "009999", "follow_request_sent": null, "is_translator": false, "id_str": "299862462", "favourites_count": 0, "default_profile": false, "url": "http://www.eoin.me", "contributors_enabled": false, "id": 299862462, "utc_offset": null, "profile_image_url_https": "https://si0.twimg.com/ profile_images/1380912173/ Screen_shot_2011-06-03_at_7.35.36_PM_normal.png", "profile_use_background_image": true, "listed_count": 0, "followers_count": 9, "lang": "en", "profile_text_color": "333333", "protected": false, "profile_background_image_url_https": "https:// si0.twimg.com/images/themes/theme14/bg.gif", "description": "Eoin's photography account. See @mceoin for tweets.", "geo_enabled": false, "verified": false, "profile_background_color": "131516", "time_zone": null, "notifications": null, "statuses_count": 255, "friends_count": 0, "default_profile_image": false, "profile_background_image_url": "http:// a1.twimg.com/images/themes/theme14/bg.gif", "screen_name": "imeoin", "following": null, "show_all_inline_media": false }, "in_reply_to_screen_name": "twitter", "in_reply_to_status_id": null } GET http://api.twitter.com/1/statuses/show.json?id=112652479837110273&include_entities=true But there’s also some information about the users mentioned in the tweet so that we can look them up if needs be.
  100. 117.

    { "coordinates": null, "created_at": "Sat Sep 10 22:23:38 +0000 2011",

    "truncated": false, "favorited": false, "id_str": "112652479837110273", "entities": { "urls": [ { "expanded_url": "http://instagr.am/p/MuW67/", "url": "http://t.co/6J2EgYM", "indices": [ 67, 86 ], "display_url": "instagr.am/p/MuW67/" } ], "hashtags": [ { "text": "tcdisrupt", "indices": [ 32, 42 ] } ], "user_mentions": [ { "name": "Twitter", "id_str": "783214", "id": 783214, "indices": [ 0, 8 ], "screen_name": "twitter" }, { "name": "Picture.ly", "id_str": "334715534", "id": 334715534, "indices": [ 15, 28 ], "screen_name": "SeePicturely" }, { "name": "Bosco So", "id_str": "14792670", "id": 14792670, "indices": [ 46, 58 ], "screen_name": "boscomonkey" }, { "name": "Taylor Singletary", "id_str": "819797", "id": 819797, "indices": [ 59, 66 ], "screen_name": "episod" } ] }, "in_reply_to_user_id_str": "783214", "text": "@twitter meets @seepicturely at #tcdisrupt cc.@boscomonkey @episod http://t.co/6J2EgYM", "contributors": null, "id": 112652479837110273, "retweet_count": 0, "in_reply_to_status_id_str": null, "geo": null, "retweeted": false, "possibly_sensitive": false, "in_reply_to_user_id": 783214, "place": null, "source": "<a href=\"http://instagr.am\" rel= \"nofollow\">Instagram</a>", "user": { "profile_sidebar_border_color": "eeeeee", "profile_background_tile": true, "profile_sidebar_fill_color": "efefef", "name": "Eoin McMillan ", "profile_image_url": "http://a1.twimg.com/ profile_images/1380912173/ Screen_shot_2011-06-03_at_7.35.36_PM_normal.png", "created_at": "Mon May 16 20:07:59 +0000 2011", "location": "Twitter", "profile_link_color": "009999", "follow_request_sent": null, "is_translator": false, "id_str": "299862462", "favourites_count": 0, "default_profile": false, "url": "http://www.eoin.me", "contributors_enabled": false, "id": 299862462, "utc_offset": null, "profile_image_url_https": "https://si0.twimg.com/ profile_images/1380912173/ Screen_shot_2011-06-03_at_7.35.36_PM_normal.png", "profile_use_background_image": true, "listed_count": 0, "followers_count": 9, "lang": "en", "profile_text_color": "333333", "protected": false, "profile_background_image_url_https": "https:// si0.twimg.com/images/themes/theme14/bg.gif", "description": "Eoin's photography account. See @mceoin for tweets.", "geo_enabled": false, "verified": false, "profile_background_color": "131516", "time_zone": null, "notifications": null, "statuses_count": 255, "friends_count": 0, "default_profile_image": false, "profile_background_image_url": "http:// a1.twimg.com/images/themes/theme14/bg.gif", "screen_name": "imeoin", "following": null, "show_all_inline_media": false }, "in_reply_to_screen_name": "twitter", "in_reply_to_status_id": null } GET http://api.twitter.com/1/statuses/show.json?id=112652479837110273&include_entities=true There’s some information about the tweet being replied to, so you can link it with the list of users
  101. 118.

    { "coordinates": null, "created_at": "Sat Sep 10 22:23:38 +0000 2011",

    "truncated": false, "favorited": false, "id_str": "112652479837110273", "entities": { "urls": [ { "expanded_url": "http://instagr.am/p/MuW67/", "url": "http://t.co/6J2EgYM", "indices": [ 67, 86 ], "display_url": "instagr.am/p/MuW67/" } ], "hashtags": [ { "text": "tcdisrupt", "indices": [ 32, 42 ] } ], "user_mentions": [ { "name": "Twitter", "id_str": "783214", "id": 783214, "indices": [ 0, 8 ], "screen_name": "twitter" }, { "name": "Picture.ly", "id_str": "334715534", "id": 334715534, "indices": [ 15, 28 ], "screen_name": "SeePicturely" }, { "name": "Bosco So", "id_str": "14792670", "id": 14792670, "indices": [ 46, 58 ], "screen_name": "boscomonkey" }, { "name": "Taylor Singletary", "id_str": "819797", "id": 819797, "indices": [ 59, 66 ], "screen_name": "episod" } ] }, "in_reply_to_user_id_str": "783214", "text": "@twitter meets @seepicturely at #tcdisrupt cc.@boscomonkey @episod http://t.co/6J2EgYM", "contributors": null, "id": 112652479837110273, "retweet_count": 0, "in_reply_to_status_id_str": null, "geo": null, "retweeted": false, "possibly_sensitive": false, "in_reply_to_user_id": 783214, "place": null, "source": "<a href=\"http://instagr.am\" rel= \"nofollow\">Instagram</a>", "user": { "profile_sidebar_border_color": "eeeeee", "profile_background_tile": true, "profile_sidebar_fill_color": "efefef", "name": "Eoin McMillan ", "profile_image_url": "http://a1.twimg.com/ profile_images/1380912173/ Screen_shot_2011-06-03_at_7.35.36_PM_normal.png", "created_at": "Mon May 16 20:07:59 +0000 2011", "location": "Twitter", "profile_link_color": "009999", "follow_request_sent": null, "is_translator": false, "id_str": "299862462", "favourites_count": 0, "default_profile": false, "url": "http://www.eoin.me", "contributors_enabled": false, "id": 299862462, "utc_offset": null, "profile_image_url_https": "https://si0.twimg.com/ profile_images/1380912173/ Screen_shot_2011-06-03_at_7.35.36_PM_normal.png", "profile_use_background_image": true, "listed_count": 0, "followers_count": 9, "lang": "en", "profile_text_color": "333333", "protected": false, "profile_background_image_url_https": "https:// si0.twimg.com/images/themes/theme14/bg.gif", "description": "Eoin's photography account. See @mceoin for tweets.", "geo_enabled": false, "verified": false, "profile_background_color": "131516", "time_zone": null, "notifications": null, "statuses_count": 255, "friends_count": 0, "default_profile_image": false, "profile_background_image_url": "http:// a1.twimg.com/images/themes/theme14/bg.gif", "screen_name": "imeoin", "following": null, "show_all_inline_media": false }, "in_reply_to_screen_name": "twitter", "in_reply_to_status_id": null } GET http://api.twitter.com/1/statuses/show.json?id=112652479837110273&include_entities=true And there’s some information that you can use to display information about the user who originated a tweet. So, be selective about what information you send down. If it’s highly redundant, then separating things out into a separate request may save data, but getting your data in the first place may be more important.
  102. 119.

    Plan for the Future One major failure that afflicts every

    project is failure to plan for the future. New features can get added, and others can be removed.
  103. 120.

    App failures are just as bad as service failures. Your

    apps should not fail just because you’ve made a change to your service -- review and release times for new apps mean that you can’t just change things ad-hoc any more.
  104. 121.
  105. 122.

    Do this from the start. And make sure that your

    FIRST version has a version attached to it.
  106. 123.

    • You can break backwards compatability. • Old, unupdated apps

    can still work. This means that you can break backwards compatibility with your APIs, and apps that rely on these APIs can still work -- users aren’t required to update!
  107. 126.

    HTTP Headers GET http://api.foobar.com/flozzles Content-Type: application/json GET http://api.foobar.com/flozzles Content-Type: application/json+foobarv1

    The other way is to make use of the Content-Type header. You can add custom suffices to the end of MIME types -- use this to identify your custom JSON schemas.
  108. 129.

    Part 3 Be Useful Finally, design your APIs to be

    useful. Make sure that consumers of APIs can do what they want, and easily.
  109. 130.

    Replication Another important topic is replication. Your API should allow

    your apps to easily replicate functionality of another application or web site. It should be trivial to do this.
  110. 131.

    Expose EVERYTHING So, as a rule, expose as much as

    possible. If you have functionality, make an API call for it, or add the functionality to an existing call.
  111. 132.

    Design an API in tandem with Your first application, your

    web site, everything you ever design, etc, etc, etc, etc Design your APIs at the same time as you design your apps/websites etc.
  112. 133.

    Make your website etc use your API If you can’t

    make your website work using your API then your mobile app can’t do it either. The ideal approach is to make sure that your traditional apps make use of the API and not direct database calls. Chances are that if a website doesn’t work when using only the API, then a mobile app will fail even more.
  113. 135.

    JSON and XML actually pretty similar so support both! As

    it turns out, you can serve basically the same data structures in both XML and JSON. It’s easy enough to support both (using the Content-Type header for instance). If an API supports XML then your Pointy-haired bosses in a C# shop might be more happy to use it -- politics!
  114. 137.

    SOAP Violently protest against the use of SOAP. SOAP on

    mobile devices is AWFUL, and will make your work far too difficult. It also ties you into using XML, and lockin is a bad thing. Many arguments against SOAP on iOS, to hear all of them, chat to Josh Deprez.
  115. 138.

    Don’t replicate your database structure Don’t replicate your database structure

    -- an API is meant to be useful to your developers, and your data model is meant to be useful to a database -- we use JOINs to make these more tolerable... but...
  116. 141.

    Use HTTP Properly HTTP is really well defined, and highly

    expressive. Using things that are built into HTTP instead of rolling your own is a great way to make your API discoverable.
  117. 142.

    Use methods appropriately GET is for getting things POST is

    for changing things PUT is for creating new things DELETE is for deleting things Use HTTP methods well -- GET, PUT, POST and DELETE all have given semantics, and they’ll tell you how things should behave.
  118. 143.

    Use status codes properly 307s for temporary redirects 303s for

    permanent redirects Use status codes -- there are many different types of redirects, 303s allow you to exploit the cache effectively, for instance.
  119. 144.

    Use status codes properly 404 for temporary “not found” errors

    410 for permanent “deleted” errors There are multiple “I’M NOT HERE” codes -- use these well: serve 410s if a particular resource will never exist again -- this influences the cache, which is useful.
  120. 145.

    Use status codes properly 418 to indicate that your web

    server is actually a teapot (see RFC 2234) RFC 2324 And if your web server is a teapot, make sure that your clients know this.
  121. 146.

    The End! And that’s the end of the talk. Hopefully

    you found something useful in it.
  122. 147.

    The End Questions? chris.neugebauer.id.au ✪ chrisjrn@gmail.com @chrisjrn ✪ speakerdeck.com/chrisjrn ALL

    THIS TECHNOLOGY IS MAKING US ANTISOCIAL Questions? Office Hours: 3:00pm