Self-sufficient API types
Networking, and interfacing with a restful backend specifically, is a solved problem. It rarely changes enough to require custom implementation, which is why the use of libraries that do the busy work is common practice. They’re tested, and their underlying mechanisms and code don’t change (now that Swift is stable). Why bother doing it yourself?
I already have a list of 15+ topics I’d like to eventually write about, and the answer to this question is the general theme of them all. For almost any problem you need solved, there will be code publicly and freely available that already solves it. I reference plenty of these solutions; I’ve relied on some of them even. But when I have the option, I rewrite them. Not due to any sort of licensing or ownership concern.
I write my own code because I enjoy it, and because it gives me confidence in the code and subsequently in myself.
If I can reimplement it, I know I understand it. And once I understand it, I can tailor it to my specific use cases. This then has a number of positive effects (which I’ll discuss later). So I stopped depending on networking libraries and started writing the code myself. Once I did, I realized it’s not that arduous, even while ensuring safety. Not as safe as the big libraries, but that’s not the point.
I chose this topic out of the list of others to write on first because I think it’s the hardest case to make, but also the best one for it. Pull back the curtain just a little bit and you start to see that networking isn’t hard (thanks to Foundation), and that doing it yourself can be immensely fulfilling.
API.swift
Foundation’s networking APIs are robust, but one of the worst parts of interfacing with it directly are the completion handlers of the NSURLSession
task-returning methods. They’re still stuck in the thing-or-nil pattern, and there are three *things* to check. Their documentation makes some guarantees about them, but they’ll still quickly wreck the clarity of your code if you let them.
This is easily avoided with a simple extension method. If you don’t need the task object either, you can abstract that away here as well. The new method is named dataRequest
to evoke this distinction:
Next step is to define the errors that can occur while processing a request. These typically fall into three main categories:
BackendStatusCode
in this case is a stand-in for the type that you’ll presumably define elsewhere for representing errors specific to your backend that are communicated to the iOS client in their response bodies. These errors are notably not defined in this interface because this would betray the categorization, which aids in pathing for error handling at call sites. Generally, the specific backend errors will be events that you’ll want to know about and directly communicate to the user, whereas the other two are likely to be at least partially handled before reaching the call site.
Endpoints
With the groundwork laid, the core work can begin. Requests have numerous components that must be defined before they can be executed. It’s easiest to coalesce them into a consistent interface for providers to adopt:
Another frustrating aspect of working with Foundation is that URLRequest
http methods are still string-ly typed. The API.HttpMethod
type included above is defined to fill that gap:
From this interface alone, a fully configured URLRequest
can now be built, filling in backend specific information as needed:
Now in, say, a Feed.swift file, endpoints related to the central feed of the app can be declared:
Underneath, we provide the fields required by Endpoint
for each of the endpoints we’ve declared:
Notably, there are some endpoints on a user mixed in with this Feed type. This is personal preference, but since these user endpoints relate directly to the app’s Feed, I might put them here instead of on a User enum of endpoints.
More importantly though, there is a concerning issue of the use of try!
in the encoding of the httpBody
for the createEvent
endpoint. We should wrap this in a do-catch statement and handle the encoding error should one occur. But then, what if we need to perform special encoder configuration specific to our API/backend? We would need to include that here as well, and then duplicate it for each endpoint that needs to JSON encode its body.
Ideally, we would pass the object up to the caller of
httpBody
by defining it asEncodable
instead ofData?
. But unfortunately this is currently impossible in Swift; with the concrete type information lost, the receiver cannot execute the call toencode
, and converting this property to a generic function is a rabbit hole not worth falling through (I tried). A type-erased AnyCodable object could fill this gap, but that’s a lot of extra effort right now.
Instead, define a new static function on API to handle JSON encoding on behalf of its endpoints. Personally, I would include this along with the prior protocol Endpoint
definition in something like an API+construction.swift file to keep the core API file clean:
Now back in the endpoint we can do:
But what if there are object-specific JSONEncoder configurations required? I use a
CoderProviding
protocol for this purpose, which I’ll detail in a future post.
Initially, providing a public method for encoding objects felt like a betrayal of API encapsulation. After all, I wouldn’t expect to call a method for performing decoding; the API type should handle that for me!
But when I thought about it more, the two are not the same, and it even makes a certain amount of sense to make encoding a public operation, even while decoding is entirely hidden from view. Swift’s generics implementation, whether intentionally or otherwise, supports this notion. In a perfect world, one could make the method private to the API type so that only the Endpoints could use it, but this isn’t possible in Swift’s current access control capabilities.
Requests
To start executing requests to the endpoints, there needs to be a method for accepting them:
This is our standard function for performing all HTTP requests using our Endpoint
s. It executes them using the dataRequest
method we defined earlier. It handles decoding and error parsing as well as dispatching to the main thread on behalf of the requestor, leaving them to only worry about what type to decode the data into, and what high level API.Error
s to handle.
The function is generic on the Success
type of the completion’s Result
parameter. This means that callers must ensure their completions are explicit, which isn’t always a nice thing to demand inside of, say, a view controller. To ease this, I like to define a static function for each of my endpoints:
This looks like boilerplate, because it kind of is. But I think it’s worthwhile boilerplate, because it cleans up call sites while declaring to consumers the functional interface of an Endpoint
, which otherwise exists as a list of static values. It can also be more than boilerplate when a request should perform some specific logic (such as input validation or result sorting) that callers shouldn’t have to manage.
A downside to this pattern however is that these methods and the Endpoint
cases they abstract are both static on the type which creates competition during autocomplete. I attempt to alleviate this by prefixing all of the functions with their HTTP method. This disambiguates the endpoint definitions from their functions while also establishing an expectation with the consumers that they can always begin typing the HTTP method and get the autocompleted list of the available functions for endpoints that use those methods.
Usage
We now have all of the foundation laid, we can begin executing our backend-specific API requests, which looks like this:
This example is fairly contrived, but it’s just for showing the fruits of all of our labor: a very clean interface for executing our requests and switching on their errors. How and where you perform these actions is up to your app’s design; the API type doesn’t care. And likewise, you don’t have care about how the API type gives you what you want. They’re decoupled.
Best of all, you get to own both sides of that fence, not just one of them. And it didn’t even require that much effort. From here, you can continue trivially defining new Endpoint
types for each subsection of your backend’s API. We haven’t implemented any tests yet, but that’s not hard to do from this point either.
There’s also lots of room for refinements inside of this structure. Custom decoders, specific error handling (and short circuiting), standard response wrapper types, completion adapters, etc. I use custom interfaces for models that define their decoding behavior, and the logic for it is entirely encapsulated in my API type. It’s not a chore to implement these things when you control the whole stack.
In fact, it’s downright fun.
Next up
I have no shortage of fun things I could delve into for the next post and I haven’t decided what it will be yet. Custom view bindings, UI presentation abstractions, view controller design, finessing this API type, or something else. More than likely, I’ll start writing at least 3 different posts before I commit to one.