Designing api for mobile apps

Wojtek Erbetowski

@erbetowski

tech lead @ polidea

Polidea...


  • makes mobile apps (iOS, Android, WP)
  • develops hardware related stuff
  • contributes to OSS - RoboSpock, Flow, ...
  • supports community
    • Mobile Central Europe
    • Mobile Warsaw

Welcome

to the world of ...

Latency

Latency


PC + WiFi Mobile + WiFi Mobile + LTE Mobile + HSDPA+ Mobile + 2G
min 0.993 2 15 42 203
avg 3.435 19 90 60 227
max 30.678 460 811 956 2000
stddev 2.22 17 x 55 84
down 139.90 18.57 14.47 5.73 0.08
up 86.19 18.23 5.73 3.68 0.1

HATEOAS

GET /users/123
{
  "name": "John Doe",
  "age": 25,
  "links": [
    {
      "rel": "self",
      "href": "/users/123"
    },
    {
      "rel": "account",
      "href": "/accounts/987"
    },
    {
      "rel": "address",
      "href": "/addresses/555"
    }
  ]
}
GET /addresses/555
{
  "street": "Sesame",
  "no": 25,
  "zipCode": "12-321",
  "state": "NY",
  "country": "US"
}

Merging responses


GET /users/123

{
  "name": "John Doe",
  "age": 25,
  "country": "US"
}

Expansion


GET /users/123?fields=[name,age,address[country]]

{
  "name": "John Doe",
  "age": 25,
  "country": "US"
}

Connection: Keep-Alive

Throughput

Connection speed market share

Data subset

Complete model
{
  "person": {
    "id": 12345,
    "firstName": "John",
    "lastName": "Doe",
    "age": 25,
    "phones": {
      "home": "800-123-4567",
      "work": "888-555-0000",
      "cell": "877-123-1234"
    },
    "email": [
      "jd@example.com",
      "jd@example.org"
    ],
    "dateOfBirth": "1980-01-02T00:00:00.000Z",
    "registered": true,
    "emergencyContacts": [
      {
        "name": "",
        "phone": "",
        "email": "",
        "relationship": "spouse|parent|child|other"
      }
    ],
    "address": {
        "street": "Sesame",
        "no": 25,
        "zipCode": "12-321",
        "state": "NY",
        "country": "US"
    }
  }
}
Trimmed model
{
    "firstName": "John",
    "lastName": "Doe",
    "age": 25,
    "country": "US"
}

Compress data (gzip)

Full: 222910 bytes
GZIP: 32128 bytes (14%)
{
"people": [
    {
        "firstName": "Jason",
        "lastName": "Page",
        "username": "jasonp",
        "isMale": true,
        "phone": "142-808-3743",
        "nid": "12252671714"
    },
    {
        "firstName": "Nathaniel",
        "lastName": "Chaney",
        "username": "nchaney",
        "isMale": true,
        "phone": "481-659-731",
        "nid": "55122958778"
    },
    {
        "firstName": "Alexis",
        "lastName": "Christensen",
        "username": "alexisc",
        "isMale": false,
        "phone": "802-046-8689",
        "nid": "13240667404"
    },
    {
        "firstName": "Sophie",
        "lastName": "Duke",
        "username": "sophied",
        "isMale": false,
        "phone": "316-701-3331",
        "nid": "96071009282"
    },
    {
        "firstName": "Aria",
        "lastName": "Wolf",
        "username": "ariaw",
        "isMale": false,
        "phone": "682-172-2915",
        "nid": "12231830224"
    },
    {
        "firstName": "Sebastian",
        "lastName": "Hogan",
        "username": "sebastianh",
        "isMale": true,
        "phone": "096-033-8930",
        "nid": "65082565494"
    },
    {
        "firstName": "Austin",
        "lastName": "Rice",
        "username": "austinr",
        "isMale": true,
        "phone": "558-871-007",
        "nid": "91102544132"
    },
    {
        "firstName": "Elijah",
        "lastName": "Savage",
        "username": "esavage",
        "isMale": true,
        "phone": "009-900-8985",
        "nid": "10301742394"
    },
    {
        "firstName": "Colton",
        "lastName": "Morris",
        "username": "cmorris",
        "isMale": true,
        "phone": "029-957-8223",
        "nid": "84062095076"
    },
    {
        "firstName": "Juan",
        "lastName": "Bass",
        "username": "jbass",
        "isMale": true,
        "phone": "701-810-3457",
        "nid": "10311068954"
    },
    {
        "firstName": "Brody",
        "lastName": "Reese",
        "username": "breese",
        "isMale": true,
        "phone": "320-610-636",
        "nid": "82112959596"
    },
    {
        "firstName": "Stella",
        "lastName": "Bernard",
        "username": "sbernard",
        "isMale": false,
        "phone": "134-462-1924",
        "nid": "03272935528"
    },
    {
        "firstName": "Isaac",
        "lastName": "Webb",
        "username": "iwebb",
        "isMale": true,
        "phone": "136-000-422",
        "nid": "10231649814"
    },
    {
        "firstName": "Jackson",
        "lastName": "Bryan",
        "username": "jacksonb",
        "isMale": true,
        "phone": "629-900-7729",
        "nid": "83071189716"
    },
    {
        "firstName": "Liam",
        "lastName": "Beard",
        "username": "lbeard",
        "isMale": true,
        "phone": "327-639-010",
        "nid": "86050552116"
    },
    {
        "firstName": "Parker",
        "lastName": "Duffy",
        "username": "pduffy",
        "isMale": true,
        "phone": "484-829-6473",
        "nid": "56122687578"
    },
    {
        "firstName": "Lucy",
        "lastName": "Moody",
        "username": "lucym",
        "isMale": false,
        "phone": "723-670-5321",
        "nid": "77051444320"
    },
    {
        "firstName": "Aaliyah",
        "lastName": "Leonard",
        "username": "aleonard",
        "isMale": false,
        "phone": "005-293-048",
        "nid": "08281195048"
    },
    {
        "firstName": "Brody",
        "lastName": "Norris",
        "username": "brodyn",
        "isMale": true,
        "phone": "789-653-2217",
        "nid": "58050880078"
    },
    {
        "firstName": "Juan",
        "lastName": "Valenzuela",
        "username": "juanv",
        "isMale": true,
        "phone": "060-065-140",
        "nid": "93052612612"
    },
    {
        "firstName": "Jocelyn",
        "lastName": "Collins",
        "username": "jcollins",
        "isMale": false,
        "phone": "010-257-964",
        "nid": "65083166064"
    },
    {
        "firstName": "Bentley",
        "lastName": "Welch",
        "username": "bentleyw",
        "isMale": true,
        "phone": "068-620-269",
        "nid": "09293076518"
    }
]
}
            
Full: 48 bytes
GZIP: 60 bytes (125%)
{
  "firstName": "Arianna",
  "lastName": "Mcknight"
}
            

Expansion

GET /people/
{
"people": [
    {
        "firstName": "Jason",
        "lastName": "Page",
        "username": "jasonp",
        "isMale": true,
        "phone": "142-808-3743",
        "nid": "12252671714"
    },
    {
        "firstName": "Nathaniel",
        "lastName": "Chaney",
        "username": "nchaney",
        "isMale": true,
        "phone": "481-659-731",
        "nid": "55122958778"
    },
    {
        "firstName": "Alexis",
        "lastName": "Christensen",
        "username": "alexisc",
        "isMale": false,
        "phone": "802-046-8689",
        "nid": "13240667404"
    },
    {
        "firstName": "Sophie",
        "lastName": "Duke",
        "username": "sophied",
        "isMale": false,
        "phone": "316-701-3331",
        "nid": "96071009282"
    },
    {
        "firstName": "Aria",
        "lastName": "Wolf",
        "username": "ariaw",
        "isMale": false,
        "phone": "682-172-2915",
        "nid": "12231830224"
    },
    {
        "firstName": "Sebastian",
        "lastName": "Hogan",
        "username": "sebastianh",
        "isMale": true,
        "phone": "096-033-8930",
        "nid": "65082565494"
    },
    {
        "firstName": "Austin",
        "lastName": "Rice",
        "username": "austinr",
        "isMale": true,
        "phone": "558-871-007",
        "nid": "91102544132"
    },
    {
        "firstName": "Elijah",
        "lastName": "Savage",
        "username": "esavage",
        "isMale": true,
        "phone": "009-900-8985",
        "nid": "10301742394"
    },
    {
        "firstName": "Colton",
        "lastName": "Morris",
        "username": "cmorris",
        "isMale": true,
        "phone": "029-957-8223",
        "nid": "84062095076"
    },
    {
        "firstName": "Juan",
        "lastName": "Bass",
        "username": "jbass",
        "isMale": true,
        "phone": "701-810-3457",
        "nid": "10311068954"
    },
    {
        "firstName": "Brody",
        "lastName": "Reese",
        "username": "breese",
        "isMale": true,
        "phone": "320-610-636",
        "nid": "82112959596"
    },
    {
        "firstName": "Stella",
        "lastName": "Bernard",
        "username": "sbernard",
        "isMale": false,
        "phone": "134-462-1924",
        "nid": "03272935528"
    },
    {
        "firstName": "Isaac",
        "lastName": "Webb",
        "username": "iwebb",
        "isMale": true,
        "phone": "136-000-422",
        "nid": "10231649814"
    },
    {
        "firstName": "Jackson",
        "lastName": "Bryan",
        "username": "jacksonb",
        "isMale": true,
        "phone": "629-900-7729",
        "nid": "83071189716"
    },
    {
        "firstName": "Liam",
        "lastName": "Beard",
        "username": "lbeard",
        "isMale": true,
        "phone": "327-639-010",
        "nid": "86050552116"
    },
    {
        "firstName": "Parker",
        "lastName": "Duffy",
        "username": "pduffy",
        "isMale": true,
        "phone": "484-829-6473",
        "nid": "56122687578"
    },
    {
        "firstName": "Lucy",
        "lastName": "Moody",
        "username": "lucym",
        "isMale": false,
        "phone": "723-670-5321",
        "nid": "77051444320"
    },
    {
        "firstName": "Aaliyah",
        "lastName": "Leonard",
        "username": "aleonard",
        "isMale": false,
        "phone": "005-293-048",
        "nid": "08281195048"
    },
    {
        "firstName": "Brody",
        "lastName": "Norris",
        "username": "brodyn",
        "isMale": true,
        "phone": "789-653-2217",
        "nid": "58050880078"
    },
    {
        "firstName": "Juan",
        "lastName": "Valenzuela",
        "username": "juanv",
        "isMale": true,
        "phone": "060-065-140",
        "nid": "93052612612"
    },
    {
        "firstName": "Jocelyn",
        "lastName": "Collins",
        "username": "jcollins",
        "isMale": false,
        "phone": "010-257-964",
        "nid": "65083166064"
    },
    {
        "firstName": "Bentley",
        "lastName": "Welch",
        "username": "bentleyw",
        "isMale": true,
        "phone": "068-620-269",
        "nid": "09293076518"
    }
]
}
            
GET /people?fields=[firstName, lastName]
{
"people": [
    {
        "firstName": "Kennedy",
        "lastName": "Cote"
    },
    {
        "firstName": "Jocelyn",
        "lastName": "Parsons"
    },
    {
        "firstName": "Mia",
        "lastName": "Key"
    },
    {
        "firstName": "Jackson",
        "lastName": "Whitney"
    },
    {
        "firstName": "Nevaeh",
        "lastName": "Torres"
    },
    {
        "firstName": "Samuel",
        "lastName": "Best"
    },
    {
        "firstName": "Ellie",
        "lastName": "Hooper"
    },
    {
        "firstName": "Kevin",
        "lastName": "White"
    },
    {
        "firstName": "Xavier",
        "lastName": "Wooten"
    },
    {
        "firstName": "Naomi",
        "lastName": "Chan"
    },
    {
        "firstName": "Owen",
        "lastName": "Ramsey"
    },
    {
        "firstName": "Victoria",
        "lastName": "Hunter"
    },
    {
        "firstName": "Harper",
        "lastName": "Boone"
    },
    {
        "firstName": "Nicholas",
        "lastName": "Mcknight"
    },
    {
        "firstName": "Kaylee",
        "lastName": "Stone"
    },
    {
        "firstName": "Hudson",
        "lastName": "Schultz"
    },
    {
        "firstName": "Thomas",
        "lastName": "Copeland"
    },
    {
        "firstName": "Madeline",
        "lastName": "Odom"
    },
    {
        "firstName": "Colton",
        "lastName": "Humphrey"
    },
    {
        "firstName": "Alexis",
        "lastName": "Chandler"
    }
]
}
            

XML vs JSON

PLAIN 171120 bytes
GZIP  30855  bytes

  
    Grayson
    Wilkins
    graysonw
    true
    197-391-9087
    39113050916
  
  
    Nathaniel
    Page
    npage
    true
    922-083-8066
    87061196536
  
  
    Annabelle
    Parks
    aparks
    false
    860-706-550
    10221048164
  
  
    Carlos
    Robbins
    crobbins
    true
    633-199-3647
    85022650976
  
  
    Sebastian
    Mckenzie
    sebastianm
    true
    031-821-2542
    08221481578
  
  
    Aiden
    Mercado
    aidenm
    true
    413-006-9553
    01302174138
  
  
    Jacob
    Terry
    jacobt
    true
    674-749-7019
    09312515478
  
  
    Hannah
    Hart
    hhart
    false
    489-395-5946
    62042618284
  
  
    Jaxon
    Wagner
    jaxonw
    true
    500-770-244
    71081634910
  
  
    Chloe
    Sanders
    csanders
    false
    714-971-5327
    12302121384
  
  
    Christopher
    Brennan
    christopherb
    true
    272-938-005
    06253116718
  
  
    Lydia
    Montoya
    lmontoya
    false
    106-629-312
    13221541804
  
  
    Adrian
    Carver
    acarver
    true
    355-762-7781
    97111290392
  
  
    Samuel
    Mccullough
    samuelm
    true
    382-502-589
    66123176094
  
  
    Landon
    Grimes
    lgrimes
    true
    082-649-575
    10272237014
  
  
    Isabella
    Merrill
    isabellam
    false
    795-762-4257
    12292864824
  
  
    Landon
    Dale
    ldale
    true
    959-023-135
    41081330132
  
  
    Madeline
    Spence
    mspence
    false
    116-600-1377
    54070686488
  
  
    Damian
    Henderson
    damianh
    true
    091-797-810
    37091971136
  
  
    Dominic
    Hurley
    dominich
    true
    842-593-567
    12281078514
  
  
    Mackenzie
    Cooper
    mcooper
    false
    335-422-488
    12292859744
  
  
    Carson
    Barnes
    cbarnes
    true
    649-653-776
    13231549814
  
  
    Elizabeth
    Sutton
    esutton
    false
    205-975-232
    86070205666
  

            
PLAIN 121115 bytes (70%)
GZIP  29553  bytes (96%)
{
"people": [
    {
        "firstName": "Jason",
        "lastName": "Page",
        "username": "jasonp",
        "isMale": true,
        "phone": "142-808-3743",
        "nid": "12252671714"
    },
    {
        "firstName": "Nathaniel",
        "lastName": "Chaney",
        "username": "nchaney",
        "isMale": true,
        "phone": "481-659-731",
        "nid": "55122958778"
    },
    {
        "firstName": "Alexis",
        "lastName": "Christensen",
        "username": "alexisc",
        "isMale": false,
        "phone": "802-046-8689",
        "nid": "13240667404"
    },
    {
        "firstName": "Sophie",
        "lastName": "Duke",
        "username": "sophied",
        "isMale": false,
        "phone": "316-701-3331",
        "nid": "96071009282"
    },
    {
        "firstName": "Aria",
        "lastName": "Wolf",
        "username": "ariaw",
        "isMale": false,
        "phone": "682-172-2915",
        "nid": "12231830224"
    },
    {
        "firstName": "Sebastian",
        "lastName": "Hogan",
        "username": "sebastianh",
        "isMale": true,
        "phone": "096-033-8930",
        "nid": "65082565494"
    },
    {
        "firstName": "Austin",
        "lastName": "Rice",
        "username": "austinr",
        "isMale": true,
        "phone": "558-871-007",
        "nid": "91102544132"
    },
    {
        "firstName": "Elijah",
        "lastName": "Savage",
        "username": "esavage",
        "isMale": true,
        "phone": "009-900-8985",
        "nid": "10301742394"
    },
    {
        "firstName": "Colton",
        "lastName": "Morris",
        "username": "cmorris",
        "isMale": true,
        "phone": "029-957-8223",
        "nid": "84062095076"
    },
    {
        "firstName": "Juan",
        "lastName": "Bass",
        "username": "jbass",
        "isMale": true,
        "phone": "701-810-3457",
        "nid": "10311068954"
    },
    {
        "firstName": "Brody",
        "lastName": "Reese",
        "username": "breese",
        "isMale": true,
        "phone": "320-610-636",
        "nid": "82112959596"
    },
    {
        "firstName": "Stella",
        "lastName": "Bernard",
        "username": "sbernard",
        "isMale": false,
        "phone": "134-462-1924",
        "nid": "03272935528"
    },
    {
        "firstName": "Isaac",
        "lastName": "Webb",
        "username": "iwebb",
        "isMale": true,
        "phone": "136-000-422",
        "nid": "10231649814"
    },
    {
        "firstName": "Jackson",
        "lastName": "Bryan",
        "username": "jacksonb",
        "isMale": true,
        "phone": "629-900-7729",
        "nid": "83071189716"
    },
    {
        "firstName": "Liam",
        "lastName": "Beard",
        "username": "lbeard",
        "isMale": true,
        "phone": "327-639-010",
        "nid": "86050552116"
    },
    {
        "firstName": "Parker",
        "lastName": "Duffy",
        "username": "pduffy",
        "isMale": true,
        "phone": "484-829-6473",
        "nid": "56122687578"
    },
    {
        "firstName": "Lucy",
        "lastName": "Moody",
        "username": "lucym",
        "isMale": false,
        "phone": "723-670-5321",
        "nid": "77051444320"
    },
    {
        "firstName": "Aaliyah",
        "lastName": "Leonard",
        "username": "aleonard",
        "isMale": false,
        "phone": "005-293-048",
        "nid": "08281195048"
    },
    {
        "firstName": "Brody",
        "lastName": "Norris",
        "username": "brodyn",
        "isMale": true,
        "phone": "789-653-2217",
        "nid": "58050880078"
    },
    {
        "firstName": "Juan",
        "lastName": "Valenzuela",
        "username": "juanv",
        "isMale": true,
        "phone": "060-065-140",
        "nid": "93052612612"
    },
    {
        "firstName": "Jocelyn",
        "lastName": "Collins",
        "username": "jcollins",
        "isMale": false,
        "phone": "010-257-964",
        "nid": "65083166064"
    },
    {
        "firstName": "Bentley",
        "lastName": "Welch",
        "username": "bentleyw",
        "isMale": true,
        "phone": "068-620-269",
        "nid": "09293076518"
    }
]
}
            

Toolbox

Client code - iOS

// PersonDataDownloader.m
#import "PersonDataDownloader_M.h"
#import "Person.h"
#import "TaskCallsSynchronizer.h"


@interface PersonDataDownloader_M ()
@property(nonatomic, strong) TaskCallsSynchronizer *synchronizer;
@end

@implementation PersonDataDownloader_M

- (void)downloadDataForPerson:(Person *)person {
    self.synchronizer = [TaskCallsSynchronizer new];
    self.synchronizer.completionHandler = ^(NSArray *taskDataObjects, NSArray *taskResponses) {
        // Merge downloaded data here and perform the rest...
    };

    self.synchronizer.errorHandler = ^(NSArray *errors) {
        NSLog(@"All calls went bad...");
    };

    NSURLSessionDataTask *personalInfoTask = [self personalInfoTaskForPerson:person synchronizer:self.synchronizer];
    NSURLSessionDataTask *accountNumberTask = [self accountNumberTaskForPerson:person synchronizer:self.synchronizer];

    [self.synchronizer addTask:personalInfoTask withName:@"InfoTask"];
    [self.synchronizer addTask:accountNumberTask withName:@"AccountTask"];

    [personalInfoTask resume];
    [accountNumberTask resume];
}

#pragma mark - Tasks

- (NSURLSessionDataTask *)personalInfoTaskForPerson:(Person *)person synchronizer:(TaskCallsSynchronizer *)synchronizer {
    NSURLSession *session = [NSURLSession sharedSession];
    __typeof(synchronizer) __weak weakSynchronizer = synchronizer;
    NSURLSessionDataTask *personalInfoTask = [session dataTaskWithURL:[self urlForPersonsPersonalInfo:person]
                                                    completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
                NSString *taskName = @"InfoTask";
                if (data && !error) {
                    [weakSynchronizer completeTaskWithName:taskName data:data response:response];
                } else {
                    [weakSynchronizer errorDidOccur:error forTaskWithName:taskName];
                }
            }];
    return personalInfoTask;
}

- (NSURLSessionDataTask *)accountNumberTaskForPerson:(Person *)person synchronizer:(TaskCallsSynchronizer *)synchronizer {
    NSURLSession *session = [NSURLSession sharedSession];
    __typeof(synchronizer) __weak weakSynchronizer = synchronizer;
    NSURLSessionDataTask *accountNumberTask = [session dataTaskWithURL:[self urlForPersonsAccountNumber:person]
                                                     completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
                NSString *taskName = @"AccountTask";
                if (data && !error) {
                    [weakSynchronizer completeTaskWithName:taskName data:data response:response];
                } else {
                    [weakSynchronizer errorDidOccur:error forTaskWithName:taskName];
                }
            }];
    return accountNumberTask;
}

#pragma mark - URLs

- (NSURL *)urlForPersonsPersonalInfo:(Person *)person {
    NSString *urlString = [NSString stringWithFormat:@"http://e1.com/persons/%ld", (long)person.id];
    return [[NSURL alloc] initWithString:urlString];
}

- (NSURL *)urlForPersonsAccountNumber:(Person *)person {
    NSString *urlString = [NSString stringWithFormat:@"http://e2.com/accounts/%ld", (long)person.id];
    return [[NSURL alloc] initWithString:urlString];
}

@end



// TaskCallSynchronizer.m
#import "TaskCallsSynchronizer.h"


@interface TaskCallsSynchronizer ()
@property(nonatomic, readonly) NSMapTable *tasksMap;
@property(nonatomic, readonly) NSMutableArray *dataObjects;
@property(nonatomic, readonly) NSMutableArray *responses;
@property(nonatomic, readonly) NSMutableArray *errors;
@property(nonatomic) BOOL errorEncountered;
@end

@implementation TaskCallsSynchronizer

- (id)init {
    self = [super init];
    if (self) {
        _tasksMap = [NSMapTable strongToWeakObjectsMapTable];
        _dataObjects = [NSMutableArray array];
        _responses = [NSMutableArray array];
        _errors = [NSMutableArray array];
    }

    return self;
}

- (void)addTask:(NSURLSessionDataTask *)task withName:(NSString *)name {
    [self.tasksMap setObject:task forKey:name];
}

- (void)completeTaskWithName:(NSString *)name data:(NSData *)data response:(NSURLResponse *)response {
    @synchronized (self) {
        NSURLSessionDataTask *task = [self.tasksMap objectForKey:name];
        BOOL isDownloadStateValid = task.state != NSURLSessionTaskStateCompleted && !self.errorEncountered;
        if (isDownloadStateValid) {
            NSLog(@"Ups, something's wrong.");
        } else {
            [self.dataObjects addObject:data];
            [self.responses addObject:response];
            [self.tasksMap removeObjectForKey:name];

            if ([self.tasksMap count] == 0 && self.completionHandler) {
                self.completionHandler(self.dataObjects, self.responses);
            }
        }
    }
}

- (void)errorDidOccur:(NSError *)error forTaskWithName:(NSString *)name {
    @synchronized (self) {
        self.errorEncountered = YES;
        [self.errors addObject:error];
        [self.tasksMap removeObjectForKey:name];

        if ([self.tasksMap count] == 0 && self.errorHandler) {
            self.errorHandler(self.errors);
        }
    }
}

@end


        

Client code - Android

protected void onResume() {
        super.onResume();
        try {
            download();
        } catch (Exception e) {
            Log.e(TAG, "Error while downloading data", e);
        }
}

public void download() throws ExecutionException, InterruptedException {
        Future<Response<JsonObject>> personFuture = Ion.
                with(this).
                load("http://e1.com/persons/123").
                asJsonObject().withResponse();

        Future<Response<JsonObject>> accountFuture = Ion.
                with(this).
                load("http://e2.com/accounts/456").
                asJsonObject().
                withResponse();

        Response<JsonObject> personResponse = personFuture.get();
        Response<JsonObject> accountResponse = accountFuture.get();

        if (personResponse.getException() != null && accountResponse.getException() != null) {

            JsonObject person = personResponse.getResult().getAsJsonObject();
            JsonObject account = personResponse.getResult().getAsJsonObject();

            String firstName = person.getAsJsonPrimitive("firstName").toString();
            String lastName = person.getAsJsonPrimitive("lastName").toString();
            String accountNo = account.getAsJsonPrimitive("accountNo").toString();

        } else {
            Log.e(TAG, "Error while downloading account", accountResponse.getException());
            Log.e(TAG, "Error while downloading person", personResponse.getException());
        }
}

Automated testing sucks!

Updating app takes long

server side version

def parts = [
    "http://e1.com/persons/123": ['firstName', 'lastName'],
    "http://e2.com/accounts/456": ['accountNo'],
]

def output = withPool(2) {
  return parts.collectParallel({ url, fields ->

    def json = new JsonSlurper().parse(url as URL)

    return fields.collect ({ field ->
      ["$field": json[field]]
    })

  }).flatten().sum()
}

Nouns vs Verbs

POST /following
{
  "from": 123,
  "to": 456
}
POST /users/456/follow
 

Common problems

versioning


  • http://api.example.com/v1
  • application/vnd.example.com.v1+json
  • http://v1.api.example.com

http cache


Expires: Sat, 26 Jul 1997 05:00:00 GMT

  • iOS (AFNetworking + NSURLCache)
  • Android (Retrofit + OkHttp + HttpResponseCache)

paging


GET /users?page=:page_number

  1. Arthur
  2. Bob
  3. Celine
  4. Daniel
  5. Eve
  6. Fred
  7. George

Page size 3

Page 1: Arthur Bob Celine

Delete: Bob

Page 1: Arthur Bobby Daniel

Page 2: Eve Fred George

relative page ref


{
  // ...
  "nextPage": "/users?since=5476238046501"
}

Case study - utest app


  • In the wild testing service
  • Testers located all over the world
  • Legacy API

Reductions


  • 36 to 20 endpoints
  • 86 to 20 calls (visiting all screens once)
  • 96% data size reduction (84% without GZIP)

Scheme

q&a