Writing a Scalable API Client in Swift 4
You can interactively follow this article with this Swift Playground.
The freshly released Swift 4 brings us some nice additions to the already nice language that we had. One of those additions is
Codable, which saves us from writing a lot of boilerplate when dealing with data parsing.
The first natural place where we can integrate it into our app is in code that make requests to remote servers. At Making Tuenti, we like to write a component to encapsulate all those networking and parsing details: an API client.
A Swifty API Client
There are some very generic requirements that we would like every good API client to satisfy:
- Its usage should be simple and straightforward.
- Changes (like new requests) should be fast to write.
- And of course, it must be Swifty.
In the open source community there are already good generic solutions. Moya is definitely Swifty and a good approach to a small API, if those are your requirements then go for it!
However, Moya’s approach doesn’t scale with medium to big teams. You end up with a single big enum type that contains lots of details. Merge conflicts within that file will certainly arise, and generally the end file will be hard to process. Some friend may say that this is what you get for violating the Open-Closed Principle.
If using an enum for this use case is not scalable for our purposes, what else does Swift offer? Protocols! Unlike with enums, with protocols you can split your code into different files and avoid painful merges, while maintaining a cohesive view of each particular request. And if you want, you can still use value types.
So there you go: we will implement a protocol-oriented API Client.
Enter the Marvel API
For this article we decided to write a client for the Marvel API. Before we dive into the code, let’s explain why we chose it:
- It’s open. Go get an API key!
- It’s sizeable. Otherwise, why make a scalable client?
- It’s real. Forcing us to deal with ugly real-world details.
- It’s consistent. OK, we don’t want so many ugly details, we want a systematic implementation too.
- It has an awesome API tester. Documentation is extensive and this online tool make it very easy to test our code.
- It’s very cool. 😎
The rest of the article we will experiment with this API using Swift Playgrounds. If you are really interested on making something useful with it, I recommend you to use the MarvelApiClient from Karumi instead.
Stateful Shell Around Functional Stateless Core
When defining a component interface, a very good rule is to base your design around stateless types with value semantics that don’t carry any side effects. Once you have that core, you can create another type that maps those types to real side effects.
Our API client component will be built around three types:
APIRequest: value types that will create the JSON request.
APIResponse: value types that will be created from the JSON response.
APIClient: will receive requests, send them to the server and then notify the caller via a callback.
That last type seems messier than the others! Well, that’s our stateful shell, and it will be the one dealing with that messy state. In reality, that component will be agnostic of most details and you should rarely need to change it.
APIClient Initial Interface
Based on the previous thoughts, we may start with something like this:
APIRequest type would look like:
resourceName is the endpoint in the Marvel API (
/comics, etc), and it’s pretty straightforward. For now we will only support
GET requests, but if we needed to send custom headers by request, we could define more attributes.
However, how would you define the parameters of such request? That’s where the weird
Encodable conformance is for. By default, any additional attributes defined in concrete
APIRequest types will be encoded and sent as parameters. We’ll see in a moment how that works.
But first, let’s see how our
APIResponse type would look like:
Thanks to that
Decodable conformance, the compiler will generate the code to decode this object from the JSON representation by using its attributes.
Refining Our Design
The previous design is simple but has several flaws:
- In the Marvel API, requests always return the same type of responses. That relation is found nowhere in our design, and that design flaw will lead to bugs and boilerplate code.
- An empty protocol is usually not a very good sign, unless you have very good reasons.
- The completion handler, since it has two optional parameters, models four possible outcomes. But we’ll have only two outcomes: success or failure.
Let’s start by changing the
Here we enter the dangerous but wonderful land of protocols with associated types. In our case we will use it to model the relation between requests and responses.
Also, notice that we don’t need
APIResponse protocol any more! Concrete
APIRequest writers will have to specify the concrete
Response type, and that type will only need to conform to
Lastly, to avoid the issues with the completion handler we will use the simple
Result type. Explaining how it works is outside of the scope of the article. There are other more advanced solutions like reactive streams, but again, out of scope.
Mixing all that, we can add some generic magic to our
Great, now it looks worse.
But take into account that you would write this code just once, since you will be changing the stateless types way more often. And this little generic piece will make those stateless types easier to write.
Creating a Request
Let’s start by implementing the first endpoint in the Marvel API,
/characters, that gives us a list of comic characters based on some (optional) input search:
First, the resource path is specified as a computed property. Why that? I concede that it’s a bit tricky, but computed properties are ignored by
Encodable, so this resource name is not encoded as a parameter. Another approach could be to filter this fake parameter out in our API client, but IMHO computed properties seems like a better fit here.
Then, all the parameters of the request are specified with the expected type. In this case there is no mandatory parameter, so all the parameters are optional. In order to create this object more easily, we add a custom
init with default values.
Finally, we see that the
Response associated type is an array of
ComicCharacter. That type is a struct that we should define outside of this request, because the Marvel API shares this model between other requests.
Decodable protocol did a lot of work for us! Did you notice that the id is non optional? Thanks to this, if we ever encounter a malformed
ComicCharacter, an error is thrown and we will be able to deal with it in any way we see fit.
However there is another type there. See the
Image type? That is not defined in any Apple framework, it’s a custom type defined by us. Let’s take a look at its code:
For some reason the Marvel API decided to send image URLs split into paths and extensions. We don’t want this parsing detail to be exposed to our business logic layers, so we try to build a correct URL from that input.
Since our input and output doesn’t match, we have to take the long road and fully implement Decodable. This means defining an
CodingKey enum and an
init. For this case, it doesn’t seem a bad tradeoff, and we will definitely reuse this type everywhere.
But this is definitely one of the bitter pills of
Codable: it’s all or nothing. You cannot define a minor detail for just one of the properties, if the default implementation isn’t right for you, you must implement everything from scratch.
In real world usage this situation is pretty common when you have default values other than nil. For example, do you want to avoid exposing nullable arrays and just convert them to empty arrays? Start typing dozens of lines of boring code.
One last detail: see that
MarvelError? For brevity’s sake we didn’t use the
DecodingError type defined in the standard library. But a battle-tested implementation should definitely use it to help deciphering programmer errors.
Sending a Request
With that previous code, this is the simplest request we can make:
See? All of our hard work meant something!
The request is succinct, the response is typed, and we only had to use standard Swift constructs. All those nasty implementation details are hidden behind our API client, and the exposed interface is nice and clean.
But we haven’t talked about an important missing piece: the actual API client component that takes these innocent value types and sends them to the Dark Dimension.
API Client Implementation
Wrapping the Response
In reality, the Marvel API is a bit more complex than what we’ve seen. The groot of every response contains information about the status of the response and it can be represented by this model:
This type does not need to be known outside of our feature, since the client will use the status and message and transform it into an
Error if needed. The important thing is that it wraps the real response… wait, what’s that
Another wrapper! The Marvel API uses this wrapper to give systematic data about returned collections, so that we can plan ahead when building our UI. Since this seems useful, we should not hide this data, and we should change our API client signature.
This is the full implementation of the final
There are lots of details here:
- There’s some magic generating the request endpoint. We’ll look at it later.
- The API client is merely a wrapper around the powerful
JSONDecoderis used to decode from the compile-time defined response type.
MarvelResponseobject is used as root for that decoding.
- In case of failure, the callback is called with a very specific error.
Creating Request Endpoints
In the Marvel API all the input information for a request is encoded in the URL. This includes not only the path but also the parameters, encoded as a URL query string.
But how do we convert those
Encodable types into a query string?
In the standard library, such job would be done by an
Encoder. This component will just map between different representations of the input data, leveraging
Codable protocol conformance.
Swift doesn’t bundle a encoder that works for URL query strings, so we found ourselves writing a custom one. Doing it in a proper way it’s a very interesting exercise, but we would need to dive in an incredibly amount of details, much more than this already long article can take.
So let’s take the hacky-but-fast route and instead write an implementation piggybacking
First we encode the original request and we receive a JSON data. Then we pass that JSON data to the
JSONDecoder, asking for a very specific type: a flat dictionary of
HTTPParameter. Finally we convert that dictionary into a string using a query string format, and we sanitise it.
It seems so hacky that it’s almost elegant!
There’s still one last detail worth mentioning: the
HTTPParameter type. The
JSONDecoder.decode(_:from:) method demands a
Decodable type in the first parameter. At first we could say: ok, let’s just convert it to
[String: Any]. But that doesn’t work because
Any does not conform to
[String: Decodable] doesn’t work because:
fatal error: Dictionary<String, Decodable> does not conform to Decodable because Decodable does not conform to itself. You must use a concrete type to encode or decode.: file /Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-900.0.65/src/swift/stdlib/public/core/Codable.swift, line 3970
So, as I was saying, I alone decided that a concrete type should be created. Let’s go with an enum with associated values. Since we only want to deal with JSON leaves, we can cover a lot just by adding four cases: strings, booleans, integers and doubles:
We need to conform to
Decodable here because we will be decoding this temporary type. It’s also an interesting exercise to see how to make a custom init for your
Decodable type using a single value container.
At last, we arrive at the uninteresting implementation of the method that generates the final endpoint URLs. It just uses our new
URLQueryEncoder and includes some authentication details:
One More Thing
Obviously, with every version of Swift we want to trash our old battle-tested code and write new untested code. So let’s not stop here and take a peek at the async/await draft proposal for Swift 5 (or 6 🤔):
If the error were propagated to an upper layer, the code would look even better, but overall it seems prettier than our current
switch based solution.
If you embrace Swift 4, your networking code can definitely be improved without the need of external libraries.
There’s still some pain points to address when parsing with custom rules, but the Swift community has a bright future and upcoming proposals can overcome them.
If you want to play with this code, remember that there is a playground ready for use with a couple interesting tricks:
MarvelAPI - Proof of concept API Client written Swift 4 to easily read the Marvel APIgithub.com