Share
Embed
Mobile APIs: More than just object.to_json
Almost all apps rely on APIs, no app is an island, entire of itself. We want to keep our apps slick and simple, pushing complex logic to the server, allowing iOS devs to focus on shiny new user experiences. At realestate.com.au, we're building simpler apps backed by smarter microservices using REST, hypermedia and HATEOAS, not just object.to_json. This session will demonstrate how to quickly build iOS apps that discover, consume and navigate these services using HAL JSON and Objective C libraries we love like AFNetworking and ReactiveCocoa in a way 'future you' will be thankful for. There will be at least one obligatory Australian reference to Crocodile Dundee.
Stewart Gleadow
November 15, 2013
Transcript
94 slides
-
Mobile APIs: More than just object.to_json Stewart Gleadow @stewgleadow REA
Group http://www.flickr.com/photos/markselliott/48699538/ -
Web services and REST ›❯ Discoverable iOS APIs ›❯ Simple
apps, smart backends ›❯ So where do I start ›❯ -
Web services and REST ›❯ Discoverable iOS APIs ›❯ Simple
apps, smart backends ›❯ So where do I start ›❯ -
POST /InStock HTTP/1.1 Host: www.example.org Content-Type: application/soap+xml; charset=utf-8 Content-Length: 299
SOAPAction: "http://www.w3.org/2003/05/soap-envelope" <?xml version="1.0"?> <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope"> <soap:Header> </soap:Header> <soap:Body> <m:GetStockPrice xmlns:m="http://www.example.org/stock"> <m:StockName>IBM</m:StockName> </m:GetStockPrice> </soap:Body> </soap:Envelope> -
We have a REST API, just construct one of these
“RESTful calls” -
Level 0 Level 1 Level 2 Level 3 The swamp
of POX URIs and Resources -
Level 0 Level 1 Level 2 Level 3 The swamp
of POX URIs and Resources HTTP verbs and status codes -
Level 0 Level 1 Level 2 Level 3 The swamp
of POX URIs and Resources HTTP verbs and status codes Hypermedia controls -
“What needs to be done to make the REST architectural
style clear on the notion that hypertext is a constraint?” Roy Fieldin - http://roy. biv.com/untan led/2008/rest-apis-must-be-hypertext-driven -
“What needs to be done to make the REST architectural
style clear on the notion that hypertext is a constraint?” Roy Fieldin - http://roy. biv.com/untan led/2008/rest-apis-must-be-hypertext-driven “Is there some broken manual somewhere that needs to be fixed?” -
“What needs to be done to make the REST architectural
style clear on the notion that hypertext is a constraint?” Roy Fieldin - http://roy. biv.com/untan led/2008/rest-apis-must-be-hypertext-driven “Is there some broken manual somewhere that needs to be fixed?” “Please try to adhere to them or choose some other buzzword for your API.” -
Web services and REST ›❯ Discoverable iOS APIs ›❯ Simple
apps, smart backends ›❯ So where do I start ›❯ -
Paths to resources within a service chan e get '/Services/Service.axd*'
do redirect to('/services') end -
{ results : [ { ... listing 1 ... },
{ ... listing 2 ... } ], _links : { next : { name : “Next page of search results”, href : “http://services.realestate.com.au/search?page=2” }, self : { name : “Refresh this page of search results”, href : “http://services.realestate.com.au/search?page=1” } } } -
POST “http://example.com/session” { email : "dundee", password : "password" }
HTTP/1.1 201 Created { user_id : “c3Rld0ByZWEtZ3Jvd” } Sign in request Sign in response -
POST “http://example.com/session” { email : "dundee", password : "password" }
HTTP/1.1 201 Created { user_id : “c3Rld0ByZWEtZ3Jvd” } Which actions are available now? Where do I perform those actions? Sign in request Sign in response -
NSDictionary *params = @{ @"email" : @"dundee", @"password" : @"password"
}; self.manager = [AFHTTPRequestOperationManager manager]; [self.manager POST:@"http://example.com/session" parameters:params success:^(AFHTTPRequestOperation *op, NSDictionary *response) { self.userId = response[@"user_id"]; } failure:nil]; Sign in & get details from an iOS client -
NSDictionary *params = @{ @"email" : @"dundee", @"password" : @"password"
}; self.manager = [AFHTTPRequestOperationManager manager]; [self.manager POST:@"http://example.com/session" parameters:params success:^(AFHTTPRequestOperation *op, NSDictionary *response) { self.userId = response[@"user_id"]; } failure:nil]; NSString *detailsUrl = [NSString stringWithFormat:@"http://example.com/details?id=%@", self.userId]; [self.manager GET:detailsUrl parameters:nil success:^(AFHTTPRequestOperation *op, NSDictionary *response) { self.name.text = response[@"name"]; self.phone.text = response[@"phone"]; } failure:nil]; Sign in & get details from an iOS client -
NSDictionary *params = @{ @"email" : @"dundee", @"password" : @"password"
}; self.manager = [AFHTTPRequestOperationManager manager]; [self.manager POST:@"http://example.com/session" parameters:params success:^(AFHTTPRequestOperation *op, NSDictionary *response) { self.userId = response[@"user_id"]; } failure:nil]; NSString *detailsUrl = [NSString stringWithFormat:@"http://example.com/details?id=%@", self.userId]; [self.manager GET:detailsUrl parameters:nil success:^(AFHTTPRequestOperation *op, NSDictionary *response) { self.name.text = response[@"name"]; self.phone.text = response[@"phone"]; } failure:nil]; Sign in & get details from an iOS client -
NSDictionary *params = @{ @"email" : @"dundee", @"password" : @"password"
}; self.manager = [AFHTTPRequestOperationManager manager]; [self.manager POST:@"http://example.com/session" parameters:params success:^(AFHTTPRequestOperation *op, NSDictionary *response) { self.userId = response[@"user_id"]; } failure:nil]; NSString *detailsUrl = [NSString stringWithFormat:@"http://example.com/details?id=%@", self.userId]; [self.manager GET:detailsUrl parameters:nil success:^(AFHTTPRequestOperation *op, NSDictionary *response) { self.name.text = response[@"name"]; self.phone.text = response[@"phone"]; } failure:nil]; Sign in & get details from an iOS client -
NSDictionary *params = @{ @"email" : @"dundee", @"password" : @"password"
}; self.manager = [AFHTTPRequestOperationManager manager]; [self.manager POST:@"http://example.com/session" parameters:params success:^(AFHTTPRequestOperation *op, NSDictionary *response) { self.userId = response[@"user_id"]; } failure:nil]; NSString *detailsUrl = [NSString stringWithFormat:@"http://example.com/details?id=%@", self.userId]; [self.manager GET:detailsUrl parameters:nil success:^(AFHTTPRequestOperation *op, NSDictionary *response) { self.name.text = response[@"name"]; self.phone.text = response[@"phone"]; } failure:nil]; Sign in & get details from an iOS client -
Response HTTP/1.1 201 Created { user_id : “c3Rld0ByZWEtZ3Jvd”, _links :
{ details : { href : “http://example.com/users/c3Rld0ByZWEtZ3Jvd/details” } } } -
Response HTTP/1.1 201 Created { user_id : “c3Rld0ByZWEtZ3Jvd”, _links :
{ details : { href : “http://example.com/users/c3Rld0ByZWEtZ3Jvd/details” } } } Which actions are available now? Where do I perform those actions? -
Response HTTP/1.1 201 Created { user_id : “c3Rld0ByZWEtZ3Jvd”, _links :
{ details : { href : “http://example.com/users/c3Rld0ByZWEtZ3Jvd/details” } } } Which actions are available now? Where do I perform those actions? -
Response HTTP/1.1 201 Created { user_id : “c3Rld0ByZWEtZ3Jvd”, _links :
{ details : { href : “http://example.com/users/c3Rld0ByZWEtZ3Jvd/details” } } } Which actions are available now? Where do I perform those actions? -
NSDictionary *params = @{ @"email" : @"dundee", @"password" : @"password"
}; self.manager = [AFHTTPRequestOperationManager manager]; [self.manager POST:@"http://example.com/session" parameters:params success:^(AFHTTPRequestOperation *op, NSDictionary *response) { self.userId = response[@"user_id"]; } failure:nil]; NSString *detailsUrl = [NSString stringWithFormat:@"http://example.com/details?id=%@", self.user_id]; [self.manager GET:detailsUrl parameters:nil success:^(AFHTTPRequestOperation *op, NSDictionary *response) { self.name.text = response[@"name"]; self.phone.text = response[@"phone"]; } failure:nil]; Sign in & get details from an iOS client -
NSDictionary *params = @{ @"email" : @"dundee", @"password" : @"password"
}; self.manager = [AFHTTPRequestOperationManager manager]; [self.manager POST:@"http://example.com/session" parameters:params success:^(AFHTTPRequestOperation *op, NSDictionary *response) { self.detailsUrl = response[@"_links"][@"details"][@"href"]; } failure:nil]; [self.manager GET:self.detailsUrl parameters:nil success:^(AFHTTPRequestOperation *op, NSDictionary *response) { self.name.text = response[@"name"]; self.phone.text = response[@"phone"]; } failure:nil]; Sign in & get details from an iOS client -
NSDictionary *params = @{ @"email" : @"dundee", @"password" : @"password"
}; self.manager = [AFHTTPRequestOperationManager manager]; [self.manager POST:@"http://example.com/session" parameters:params success:^(AFHTTPRequestOperation *op, NSDictionary *response) { self.detailsUrl = response[@"_links"][@"details"][@"href"]; } failure:nil]; [self.manager GET:self.detailsUrl parameters:nil success:^(AFHTTPRequestOperation *op, NSDictionary *response) { self.name.text = response[@"name"]; self.phone.text = response[@"phone"]; } failure:nil]; Sign in & get details from an iOS client -
NSDictionary *params = @{ @"email" : @"dundee", @"password" : @"password"
}; self.manager = [AFHTTPRequestOperationManager manager]; [self.manager POST:@"http://example.com/session" parameters:params success:^(AFHTTPRequestOperation *op, NSDictionary *response) { self.detailsUrl = response[@"_links"][@"details"][@"href"]; } failure:nil]; [self.manager GET:self.detailsUrl parameters:nil success:^(AFHTTPRequestOperation *op, NSDictionary *response) { self.name.text = response[@"name"]; self.phone.text = response[@"phone"]; } failure:nil]; Sign in & get details from an iOS client -
GET “http://example.com/services” ... HTTP/1.1 200 OK { _links : {
signIn : { name : “Authenticate a user”, href : “http://example.com/session” }, suggest : { name : “Retrieve suburb suggestions matching a string”, href : “http://suggest.realestate.com.au/suggest{?query}”, templated : true }, ... } } -
GET “http://example.com/services” ... HTTP/1.1 200 OK { _links : {
signIn : { name : “Authenticate a user”, href : “http://example.com/session” }, suggest : { name : “Retrieve suburb suggestions matching a string”, href : “http://suggest.realestate.com.au/suggest{?query}”, templated : true }, ... } } -
[self.manager GET:@"http://example.com/services" parameters:nil success:^(AFHTTPRequestOperation *op, NSDictionary *response) { self.signInEndpoint =
response[@"_links"][@"signIn"][@"href"]; self.suggestEndpoint = ... } failure:nil]; Consuming discoverable endpoints -
[self.manager GET:@"http://example.com/services" parameters:nil success:^(AFHTTPRequestOperation *op1, NSDictionary *servicesResponse) { NSString *signInEndpoint
= servicesResponse[@"_links"][@"signIn"][@"href"]; [self.manager POST:signInEndpoint parameters:params success:^(AFHTTPRequestOperation *op2, NSDictionary *signInResponse) { NSString *detailsEndpoint = signInResponse[@"_links"][@"details"][@"href"]; [self.manager GET:detailsEndpoint parameters:nil success:^(AFHTTPRequestOperation *op3, NSDictionary *response) { self.name.text = response[@"name"]; self.phone.text = response[@"phone"]; } failure:^(AFHTTPRequestOperation *op3, NSError *error) { self.status.text = @"Failed to get user details."; }]; } failure:^(AFHTTPRequestOperation *op2, NSError *error) { self.status.text = @"Failed to sign in."; }]; } failure:^(AFHTTPRequestOperation *op1, NSError *error) { self.status.text = @"Failed to configure service"; }]; Chainin multiple requests ets messy -
[self.manager GET:@"http://example.com/services" parameters:nil success:^(AFHTTPRequestOperation *op1, NSDictionary *servicesResponse) { NSString *signInEndpoint
= servicesResponse[@"_links"][@"signIn"][@"href"]; [self.manager POST:signInEndpoint parameters:params success:^(AFHTTPRequestOperation *op2, NSDictionary *signInResponse) { NSString *detailsEndpoint = signInResponse[@"_links"][@"details"][@"href"]; [self.manager GET:detailsEndpoint parameters:nil success:^(AFHTTPRequestOperation *op3, NSDictionary *response) { self.name.text = response[@"name"]; self.phone.text = response[@"phone"]; } failure:^(AFHTTPRequestOperation *op3, NSError *error) { self.status.text = @"Failed to get user details."; }]; } failure:^(AFHTTPRequestOperation *op2, NSError *error) { self.status.text = @"Failed to sign in."; }]; } failure:^(AFHTTPRequestOperation *op1, NSError *error) { self.status.text = @"Failed to configure service"; }]; Chainin multiple requests ets messy -
NSString *services = @"http://example.com/services"; RACSignal *userDetailsSignal = [[[[[[self.manager rac_getPath:services] map:^NSString
*(NSDictionary *response) { return response[@"_links"][@"signIn"][@"href"]; }] flattenMap:^RACSignal *(NSString *signInEndpoint) { return [self.manager rac_postPath:signInEndpoint parameters:params]; }] map:^NSString *(NSDictionary *response) { return response[@"_links"][@"details"][@"href"]; }] flattenMap:^RACSignal *(NSString *detailsEndpoint) { return [self.manager rac_getPath:detailsEndpoint]; }] replayLast]; ReactiveCocoa to the rescue -
NSString *services = @"http://example.com/services"; RACSignal *userDetailsSignal = [[[[[[self.manager rac_getPath:services] map:^NSString
*(NSDictionary *response) { return response[@"_links"][@"signIn"][@"href"]; }] flattenMap:^RACSignal *(NSString *signInEndpoint) { return [self.manager rac_postPath:signInEndpoint parameters:params]; }] map:^NSString *(NSDictionary *response) { return response[@"_links"][@"details"][@"href"]; }] flattenMap:^RACSignal *(NSString *detailsEndpoint) { return [self.manager rac_getPath:detailsEndpoint]; }] replayLast]; ReactiveCocoa to the rescue -
Web services and REST ›❯ Discoverable iOS APIs ›❯ Simple
apps, smart backends ›❯ So where do I start ›❯ -
http://www.flickr.com/photos/jakecaptive/3205277810 Forcin lo ic to the server What if you
could only use NSDictionary objects for your domain model? -
landSize : { size: 1440, units: “metres squared”, display: “Land
size: 1440 m²” } -
landSize : { size: 1440, units: “metres squared”, display: “Land
size: 1440 m²” } Less lo ic in the iOS client! -
features: { outdoor: [ {garage: 2}, ], indoor: [ “air-conditioning”,
“alarm-system”, ... {ensuite: 1} ] } -
features: [ { label: “Outdoor Features”, features: [ “Garage: 2”,
] }, { label: “Indoor Features”, features: [ “Air Conditioning”, “Alarm System”, ... “Ensuite: 1” ] } ] -
propertyFeatures: { display: “\e021 4 \e022 1 \e023 2” }
Could we format beds, baths, carparks using an icon font? -
photo : { thumbnail: ”http://photos.realestate.com.au/100x30/EzMjNhOU”, thumbnail-retina: ”http://photos.realestate.com.au/200x60/EzMjNhOU”, gallery: ”http://photos.realestate.com.au/320x90/EzMjNhOU”, gallery-retina:
”http://photos.realestate.com.au/640x180/EzMjNhOU”, detail: ”http://photos.realestate.com.au/480x320/EzMjNhOU”, detail-retina: ”http://photos.realestate.com.au/960x640/EzMjNhOU” } Explicit links for each photo size -
photo : { imageServer: ”http://photos.realestate.com.au”, imageId: “EzMjNhOU” } Don’t template
the URI in the iOS client -
photo : { imageServer: ”http://photos.realestate.com.au”, imageId: “EzMjNhOU” } NSInteger width
= 100, height = 60; photoUrl = [NSString stringWithFormat: @”%@/%dx%d/%@”, photo.imageServer, width, height, photo.imageId]; Don’t template the URI in the iOS client -
photo : “http://photos.realestate.com.au/{width}x{height}/EzMjNhOU” CSURITemplate *template = [CSURITemplate URITemplateWithString:photo error:&error]; NSDictionary
*params = @{ @"width" : @100, @"height" : @60 }; NSString *photoUrl = [template relativeStringWithVariables:params error:&error]; Server-side RFC-6570 URI template -
Web services and REST ›❯ Discoverable iOS APIs ›❯ Simple
apps, smart backends ›❯ So where do I start ›❯ -
Web services and REST ›❯ Discoverable iOS APIs ›❯ Simple
apps, smart backends ›❯ So where do I start ›❯ -
References John Donne, No Man Is An Island http://en.wikipedia.org/wiki/Meditation_XVII Martin
Fowler, Richardson Maturity Model http://martinfowler.com/articles/richardsonMaturityModel.html Leonard Richardson, Justice Will Take Us Millions Of Intricate Moves http://www.crummy.com/writing/speaking/2008-QCon/act3.html Roy Fieldin - REST APIs Must Be Hypertext Driven http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven Leonard Richardson, API Desi n is Stuck in 2008 http://blog.programmableweb.com/2013/10/07/api-design-is-stuck-in-2008/ Cam Barrie, Desi n your API, Mother %$^ http://vimeo.com/61342270 Mike Kelly, HAL JSON http://stateless.co/hal_specification.html -
References Perryn Fowler, Richardson Maturity Model By Example http://prezi.com/1-0gtuokjcqo/rmm-by-example/ The
Internet En ineerin Task Force (IETF), RFC 6570 - URI Template http://tools.ietf.org/html/rfc6570 Wikipedia, RSDL: RESTful Service Description Lan ua e http://en.wikipedia.org/wiki/RSDL Jim Webber, Savas Parastatidis, Ian Robinson; REST in Practice http://restinpractice.com/book/ Leonard Richardson, Sam Ruby; RESTful Web Services http://shop.oreilly.com/product/9780596529260.do