Designing api for mobile apps – Wojtek Erbetowski – @erbetowski



Designing api for mobile apps – Wojtek Erbetowski – @erbetowski

0 0


pyconpl_2014


On Github wojtekerbetowski / pyconpl_2014

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
Wojtek

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"
}
better for public APIs in contrast to homegrown, efficient expansion mechanism

Connection: Keep-Alive

Throughput

Connection speed market share

Maciek

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
GraysonWilkinsgraysonwtrue197-391-908739113050916NathanielPagenpagetrue922-083-806687061196536AnnabelleParksaparksfalse860-706-55010221048164CarlosRobbinscrobbinstrue633-199-364785022650976SebastianMckenziesebastianmtrue031-821-254208221481578AidenMercadoaidenmtrue413-006-955301302174138JacobTerryjacobttrue674-749-701909312515478HannahHarthhartfalse489-395-594662042618284JaxonWagnerjaxonwtrue500-770-24471081634910ChloeSanderscsandersfalse714-971-532712302121384ChristopherBrennanchristopherbtrue272-938-00506253116718LydiaMontoyalmontoyafalse106-629-31213221541804AdrianCarveracarvertrue355-762-778197111290392SamuelMcculloughsamuelmtrue382-502-58966123176094LandonGrimeslgrimestrue082-649-57510272237014IsabellaMerrillisabellamfalse795-762-425712292864824LandonDaleldaletrue959-023-13541081330132MadelineSpencemspencefalse116-600-137754070686488DamianHendersondamianhtrue091-797-81037091971136DominicHurleydominichtrue842-593-56712281078514MackenzieCoopermcooperfalse335-422-48812292859744CarsonBarnescbarnestrue649-653-77613231549814ElizabethSuttonesuttonfalse205-975-23286070205666
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

Arthur Bob Celine Daniel Eve Fred 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