Accurate portrayal of a programmer at work.

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: Result.

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

A DC API would be obviously worse.

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:

And our APIRequest type would look like:

The resourceName is the endpoint in the Marvel API (/characters, /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.
Back to the drawing board

Let’s start by changing the APIRequest protocol:

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 theAPIResponse protocol any more! Concrete APIRequest writers will have to specify the concrete Response type which will only need to conform to Decodable.

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 APIClient protocol:

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.

Codable is exciting

The 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

Dormammu, I’ve come to run my Swift Playground!

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 DataContainer thing?

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.

The Send Method

This is the full implementation of the final send method:

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 URLSession.
  • JSONDecoder is used to decode from the compile-time defined response type.
  • The MarvelResponse object 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 JSONEncoder:

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 String to HTTPParameter. Finally, we convert that dictionary into an array of standardURLQueryItem elements.

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 Codable. Also, [String: Decodable] doesn’t work because:

Protocol type ‘Decodable’ cannot conform to ‘Decodable’ because only concrete types can conform to protocols
Thanks, compiler! Also, I am Groot!

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.

Conclusion

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:

Happy Decoding & Encoding!