Writing a Scalable API Client in Swift 5
You can interactively follow this article with this Swift Playground.
Swift 4 brought us a very nice addition to the already nice language that we had:
Codable, which saves us from writing a lot of boilerplate when dealing with data parsing. Swift 5, although centered on ABI stability, also brought us another useful type:
The first natural place where we can integrate them into our app is in code that makes 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 makes 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 in 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 a 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 relationship between requests and responses.
Also, notice that we don’t need the
APIResponse protocol any more! Concrete
APIRequest writers will have to specify the concrete
Response type which will only need to conform to
Lastly, to avoid the issues with the completion handler we will use
Result. This new type introduced in Swift 5 in the standard library is very simple: it’s just an enum with a success case and a failure case. If you want to dive deeply into how it works, I recommend the awesome Hacking with Swift article about it.
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 the 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 a 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 an 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 incredible 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 an array of standard
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 try to directly convert to
[URLQueryItem]. But that's not possible since the JSON decoder encodes objects into a
JSON object, not an array, so the output must be Swift
Dictionary. Ok then, 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:
Protocol type ‘Decodable’ cannot conform to ‘Decodable’ because only concrete types can conform to protocols
So, as I was saying, I alone decided that a temporary 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 mixes the output of our
URLQueryItemEncoder with some authentication headers:
Notice how the usage of
URLComponents makes our code safer against bad input and allows us to generate correct URLs in an orderly way.
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 from Chris Lattner:
If the error were propagated to an upper layer, the code would look even better, but even this simple task seems a bit prettier than our current closure-based solution.
If you embrace Swift 5, your networking code can definitely be improved without needing 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 of interesting tricks:
MarvelAPI - Proof of concept API Client written Swift 4 to easily read the Marvel APIgithub.com