Crafting Evolvable API Responses
Who am I?
• Twitter: @darrel_miller• http://www.bizcoder.com/
Solve API Problems Fast
Our Journey Today
• Focus on API responses• Versioning is painful• Why we think we need it?• How can we avoid it?• What if you can’t?
Objects over the wire
We have been here before
• CORBA, DCOM• SOAP, WSDL• DTOs• JSON
The ASP.NET Web API Project Template
public class ValuesController : ApiController { // GET api/values public IEnumerable<string> Get() { return new string[] { "value1", "value2" };}
// GET api/values/5 public string Get(int id) { return "value"; }
// POST api/values public void Post([FromBody]string value) { }
// PUT api/values/5 public void Put(int id, [FromBody]string value) { }
// DELETE api/values/5 public void Delete(int id) { } }
The ASP.NET Web API Starter Tutorialpublic class ProductsController : ApiController { //…
public IEnumerable<Product> GetAllProducts() { return products; }
public IHttpActionResult GetProduct(int id) { var product = products.FirstOrDefault((p) => p.Id == id); if (product == null) { return NotFound(); } return Ok(product); } }
http://www.asp.net/web-api/overview/getting-started-with-aspnet-web-api/tutorial-your-first-web-api
ServiceStack
public class ReqstarsService : Service { public List<Reqstar> Get(AllReqstars request) { return Db.Select<Reqstar>(); } }
NancyFX
public class SampleModule : Nancy.NancyModule{ public SampleModule() { Get["/"] = _ => "Hello World!"; }}
Python Flask
@app.route('/todo/api/v1.0/tasks', methods=['GET'])def get_tasks(): return jsonify({'tasks': tasks})
Rails
# Returns the resource from the created instance variable # @return [Object] def get_resource instance_variable_get("@#{resource_name}") end
But, what’s the problem?
Which objects to map?
• Domain objects • How do we hide content we don’t want to expose?• How do we create different views of data?• Changes to domain model cause changes to API
• DTOs• whole lot of busy work• Only indirect control over serialization process
Automatic Serialization
• All the properties• Missing values, unrecognized properties• Non standard data types: datetime, timespan• Does null mean unspecified, or explicitly null?• Empty collection or no collection• Capitalization• Links• Cycles• Changes to behavior in the frameworks• Security Risks
The whole app is useless because the API added a new preference
APIs Break because Serialization Libraries Change/Fix things
Take back control
Use a DOM to build your document
dynamic jspeaker = new JObject();jspeaker.name = speakerInfo.Name;jspeaker.bio = speakerInfo.Bio;
dynamic links = new JObject();
dynamic iconLink = new JObject();iconLink.href = speakerInfo.ImageUrl;links.icon = iconLink;
dynamic sessionsLink = new JObject();sessionsLink.href = SessionsLinkHelper.CreateLink(request, speakerInfo).Target;links[LinkHelper.GetLinkRelationTypeName<SessionsLink>()] = sessionsLink;
jspeaker["_links"] = links;return new DynamicHalContent(jspeaker);
What to do with your new found freedom?
Resources and Representations
/api/order/34
/api/order/35
/api/order/35/invoice.html
application/json
application/ld+json
/api/order/35/invoice.pdf
application/json
application/ld+json
application/pdf
text/html
/api/PrintQueue
/api/CurrentWeather
/api/LeaderBoard
Anatomy of an HTTP representation
200 OK HTTP/1.1
Server: Microsoft-HTTPAPI/2.0Content-Length: 44Content-Type: text/plainAcme-perf-database-cost: 120ms
The quick brown fox jumped over the lazy dog
Let’s build some payloads
{"description" :"There is a hole in my bucket"
}
The smallest thing that is actionable
{"description" :"There is a hole in my bucket","steps_to_reproduce" : "Pour water in bucket. Lift bucket off ground.
Look for water dripping", "date_reported": "2012-04-21T18:25:43-05:00"
}
Just enough data to solve the problem
{"description" :"The font is too big","application_name" : "Wordament","application_version" : "1.2.0.2392","environment_osversion" : "NT4.0","environment_memory_free" : "100 MB","environment_diskspace_free" : "1.5 GB", "reported_by_user" : “Bob Bing”
}
Why do they need that data?
{"description" :"The font is too big","application_name" : "Wordament","application_version" : "1.2.0.2392","environment" : { "osversion" : "NT4.0",
"memory_free" : "100 MB","diskspace_free" : "1.5 GB"
}}
Attribute Groups
{"description" :"The font is too big","history" : [
{"status" : "reported", "date" :"2014-02-01"},{"status" : "triaged", "date" :"2014-02-04"},{"status" : "assigned", "date" :"2014-02-12"},{"status" : "resolved", "date" :"2014-02-19"},
]}
Attribute Groups for multiple instances
{"description" :"The font is too big","reported_by_user" : { "name" : "Bob Bing",
"email" : "[email protected]", "twitter" : "@bobbing", "date_hired" : "2001-01-21"
}}
Attribute Groups for related data
{"description" :"The font is too big","reported_by_user_url" : "http://api.acme.com/users/75"
}
Linking related data
{"description" :"The font is too big",“links" : [
{ "href" :"http://api.acme.com/users/75", "rel": "reportedbyuser" },{ "href" :"http://api.acme.com/users/24", "rel": "assignedtouser" }
] }
Multiple Links
{"issues" : [
{"description" :"There is a hole in my bucket"
}]
}
Beware of structural changes{
"issue" : {
"description" :"There is a hole in my bucket"}
}
Item in a Array
Item as a Property
Naming Your Conventions
• Empty arrays• Nulls• Single item as array or object• Camel case, snake case• Vocabulary• Protocols
Media Type Scope
application/jsonapplication/vnd.acme.issue+json
application/issue+json
Too broadToo narrowIdeal
application/hal+json Partialapplication/vnd.acmeapi+json Compromise
Vocabularies : Profiles, Namespaces, Schemas, Ontologies
Learn from others
application/hal+jsonapplication/ld+jsonapplication/vnd.mason+jsonapplication/vnd.siren+jsonapplication/vnd.amundsen-uber+json
application/vnd.github.v3+jsonapplication/vnd.heroku+json
application/vnd.api+jsonapplication/atom+xmlapplication/voicexml+xmlapplication/vnd.collection+jsonapplication/activity+jsonapplication/http-problemapplication/json-home
Generic Hypermedia API Specific Task Specific
{"description" :"The font is too big","_embedded" : {
"reportedByUser" : { "name" : "Bob Bing",
"email" : "[email protected]","_links" : { "self" : {"href" :"http://api.acme.com/users/75"}}
}
}
Meet application/hal+json
{ "collection": { "links": [], "items": [ { "data": [ { "name": "Title", "value": "\r\n\t\t\tLearning from Noda Time: a case study in API design and open source (good, bad and ugly)\r\n\t\t“ }, { "name": "Timeslot", "value": "04 December 2013 16:20 - 17:20“ }, { "name": "Speaker", "value": "Jon Skeet“ } ], "links": [ { "href": "http://conference.hypermediaapi.com/speaker/6", "rel": "http://tavis.net/rels/speaker" }, { "href": "http://conference.hypermediaapi.com/session/133/topics", "rel": "http://tavis.net/rels/topics" } ], "href": "http://conference.hypermediaapi.com/session/133" } ], "query": [], "template": { "data": [] }, "version": "1.0" }}
Meet application/vnd.collection+json
Version as a Last Resort
• Version the payload• HTML DOCTYPE, Collection+Json
• Version the resource• /api/documents/invoice.v2/758
• Version the media type• application/vnd.github.v3+json
• Version the API• /api/v2/documents/invoice/758
Wrap up
• Understand the limitations of “objects over the wire”• Consider taking back control of your representations• Think in terms of messages, instead of objects• Build software that is designed to survive change• Believe that versioning is an admission of failure
Image Credits
• Package - https://flic.kr/p/3mrNyn• Freedom - https://flic.kr/p/4vwRDw• Treasure map - https://flic.kr/p/7jDJwi• Handshake - https://flic.kr/p/nbAu8Y• Telephone - https://flic.kr/p/7Q8bMd• Blindfolded Typing - https://flic.kr/p/53Q3JE• Magic Trick - https://flic.kr/p/7T8zk5• Donut machine - https://flic.kr/p/anncxf• GT-R Steering Wheel - https://flic.kr/p/fDUSDk• Anatomy - https://flic.kr/p/6bfUZn• Shapes - https://flic.kr/p/3aKUAq• Payloaders - https://flic.kr/p/dTU9sN• Birds on a Wire - https://flic.kr/p/4YdfK