Many of our familiar practices from Rails development still apply.
- REST still provides a nice clean way to organize our routes.
- XML and JSON are common data formats.
- Clients create sessions, often identified by a session id returned in a cookie.
- We can improve client performance and reduce server load by caching request responses, providing HTTP ETags and 304 responses, and appending asset ids to assets served via the rails stack.
However we also find a number of cases where our conventions do not match well with the needs and behavior of mobile clients.
Synchronous requests and redirects:
Synchronous requests on high latency wireless networks lead to a poor user experience as clients must wait for a response from the server before they can proceed. Unfortunately for us, redirects create synchronous requests. When a client encounters a redirect response it has waited for one full round trip to the server and must now wait for another round trip before it will have the information it needs to proceed. Similarly references to other resources including stylesheets or child objects cannot be loaded until the client has received the initial response containing those references.
Whenever a client needs to make multiple requests there is a reasonable chance that some of them will fail. This can become problematic as the client then needs to determine what to do with the partial set of data it has received. Should the client retry only the failed requests and hope that the rest of the data set is still up to date by the time they finish? Should the entire set of data be discarded because one portion could not be loaded? Can an incremental or partial set of results be shown to the user and still be meaningful?
We might allow a user to sign into our API via a
sessions/#create route and expose an authenticated user’s profile via a
profile route. If we want to allow users to sign in and then be greeted by name (retrieved from their profile) we are forcing clients to either make a pair of synchronous requests before a user can move past the sign in screen or to display a reasonable interface when the users profile has not yet been retrieved.
If instead we were to return the user’s profile data as part of a successful
sessions/#create response we could cut the perceived sign in time in half.
Returning deeply nested object graphs may present us with a new problem. Now we may need to join multiple models together to construct the response and generate an expensive query in the process. That’s not too bad, we’re pretty good at optimizing query times when they become a problem, but it also makes the resulting response harder to cache because we must invalidate that cache whenever any of the models included in that aggregate response change.
On the web, sessions are well defined and we often use session cookies to store session identifiers. Mobile devices are not so simple; they are likely to gain and lose focus many times while a user is actively using the app. Every phone call, text message, switch to look at a mobile browser, and so on may force an iOS app into the background. As an app enters the background we could delete session identifiers and clear out any sensitive cached data, but we don’t know if the user will return immediately or in the far future. If we discard credentials aggressively we may force the user to re-authenticate after every interruption, a poor user experience, but if we do not we might expose those credentials to whomever next uses the app.
We might be able to conditionally clear out data from expired sessions when an app resumes but may also need to store it encrypted in order to prevent access to sensitive data while the app is not running.
Mobile clients operate on unreliable network connections. As a result many of their network requests will fail somewhere along their route to our servers. Retrying failed requests should be an obvious initial solution when a request never reaches the server but what about when the acknowledgement of a request is lost on its way back to the client? Suppose our server received a request to create a new user account with a unique username and responded with some kind of 201 (created) response. Unfortunately the response never reached the client device, it eventually considers the request to have timed out and dutifully sends it again, eventually successfully. Now our server receives a second request to create an account with the same unique username and returns a validation error because that username is already taken!
It might be a better user experience if our server could identify duplicate requests from the same client and return the same response for all of them without applying duplicate changes to our model. In general it is beneficial for us if we can make all requests to our API idempotent, resulting in the same model state no matter how many times they are performed. Therefore we should prefer an update which sets a value to an update which increments that value or include enough information in each request (timestamps, sequence numbers, version numbers, and so on) to identify duplicate requests.
Designing APIs for mobile clients presents some new problems which force us to reconsider some of our common practices as developers of web applications. Many of the conventions found in Rails are still valuable, but we need to respect the needs of our mobile clients and the network environment they operate in order to enable them to provide the best possible user experience.