Using GraphQL in Production iOS Applications – Part 2
In Part 1, we discussed what made GraphQL an attractive technology for one of our client projects. We ended up learning a good amount about the technology.
Here’s what was great, what was okay, and what was disappointing about the experience.
Schema as Contract
Initial, rapid prototyping of our backend data model was incredibly easy. All it took was an initial GraphQL schema as a contract for both teams to build against. Apollo Server can serve a simple mock server based on the schema with zero configuration, and it is straightforward to expand the mock server’s functionality.
Expanding the schema was also trivial. So long as the only changes were additive, the backend could quickly revise the API to support new properties or new objects. Existing clients could only ask for properties they already knew about, so all prior builds would just work without issue.
The frontend was able to get the data needed and nothing more with a minimum amount of requests.
What sorta worked
There’s not a GraphQL standard for authorization. Because we were building a server that only had one level of authorization for users, we used standard REST endpoints for logging in, and then gave authenticated users unrestricted access to the GraphQL queries. This wouldn’t work with a more complex security model. The lack of standard here was uncomfortable, especially in an app that needed to be HIPAA compliant. Apollo Server does suggest a solution, though.
Automatically Generated Models
These worked great and saved us a lot of time early in development. However, two requests that ask for the exact same object with the exact same properties are, in fact, two different types. While this can be solved by GraphQL’s Fragments, they begin to etch away at what made these models so easy to use in the first place. We only just reached the point where the complexity of Fragments was needed before this project ended, so it’s unclear if it’d be best to keep going with Fragments or implement our own model layer.
Instead, we worked around that by making as few requests as possible so we could share types. The impact of that was…
Unfortunately, “get all the data with minimal queries” is the antithesis of “only ask for the data you need, when you need it”. Most of our queries asked for a single model, and for all the properties on that model. That became the generated type for that model that we used throughout the app. We absolutely didn’t take advantage of batching queries together, or only asking for minimal data. Indeed, it would be exceedingly difficult to take advantage of that without a translation layer from the query-generated models into the app’s data model.
The schema is at the core of developing a GraphQL app, and it is a net gain to dev speed. However, because of its central role, non-additive modifications do require coordination between frontend and backend teams. This means that development is not entirely decoupled.
For example, changing existing types and fields broke existing clients that were built with an older schema. This isn’t surprising and on-par with the effects of making a breaking change to a REST implementation. Still, those breaking changes snuck up on us more than they should have. Additionally, every breaking schema change required a code change on the client. This was not only to rebuild the query models, but also to propagate the property names and type changes throughout the codebase.
All this highlights a gap between the promise & execution of GraphQL. While the schema is a wonderful source of truth, without perfect foreknowledge it’s actually a very sensitive dependency.
What did not work
Batching? We don’t need no stinking batching.
Despite the fact that we weren’t making any batch requests, the Apollo response could still contain both data and errors, as if we had made a batch request. This meant every request had 3 results (success, partial success, and error) rather than simply “success” & “error”. This complexity was a pain, and was unavoidable.
Caching in the Apollo iOS SDK
A sane person wouldn’t cache a response with an error. Unfortunately, that’s exactly the default configuration for caching in the Apollo iOS SDK. While that’s been reported as a bug, the lack of support forced us to choose between functional caching or a schema that allowed nullable queries, a central concept of GraphQL. Both options are likely to become unacceptable at a certain point.
Is it right for you?
In Part 3, we reflect on all we’ve learned and let you know what you should consider when using GraphQL in your project to maximize the benefit while minimizing the costs.