Generic Networking API in Swift
Create a simple and reusable Networking API in Swift using generics and the async/await pattern
If you’re building an application, probably at some point you need to communicate with a backend to send and receive data. To do this kind of task, your will need to perform network requests to the server.
Apple provides an easy way to deal with this job through the URLSession class. Before Swift 5.5, we needed to use the URLSession’s methods with completion blocks to process the results. However, from Swift 5.5 on, we can now perform network requests using the async/await pattern. If you’re not familiar with async/await in Swift, you can check this post that I wrote some time ago in which I go through all the key concepts that you need to know.
We can combine the new async/await pattern together with the power of the generic feature that Swift gives us, and create a generic networking API that serves for every network request that we need to perform.
Let’s explore how we can achieve this in a few simple steps.
First things first, we need to create a new enum to hold all the endpoints that our applications will need to interact with and some extra information that we’re going to use later on.
If we have an endpoint that requires some query parameters, we can define it in the enum itself. For example, imagine that we need to use an API to retrieve all the countries that a continent has (GET {base url}/countries?continent=america
).
We could define that endpoint as follows:
The next step is to set some pre-defined errors for our networking manager.
We are now in a good spot to start developing our generic API manager. The two methods that we’re going to use are:
- func data(for:delegate:) -> (Data, URLResponse)
- func upload(for:from:delegate:) -> (Data, URLResponse)
Let’s go ahead and create a brand new class named ApiManager
.
- The URL of the API we’re going to use.
- The URLSession instance to perform the network requests.
- An instance of JSONEncoder to encode the data that we send to the server in a POST method.
- An instance of JSONDecoder to decode the data that we get from the server.
Our API manager is going to expose two methods: getData
and sendData
. Both methods will need to create an URLRequest type to pass the URLSession's functions data(for:delegate)
and upload(for:from:delegate)
.
So, it’s a good idea to create a utility function that returns an URLRequest instance from an ApiEndpoint that we previously defined.
The missing parts are our getData and sendData functions.
A couple of things to note here:
- We defined a typealias that matches the URLSession’s method response just to add a little more semantic to the code.
- The usage of generic types in both functions. This gives us the possibility to write only one function for getting any data from the server, as long as the returned type is a Decodable type. Likewise, we write only one function to send any data to the server, as long as the data type to be sent is a Codable type, and, like the
getData
function, the return type is a Decodable type. - Both functions are marked as async and with possible error throws. This means that if the network request produces any error, this will be automatically returned to the function’s caller. So we don’t have the need anymore to handle the success and the failure case here to determine what to return.
Let’s see how our API manager actually works. We’re going to use the endpoint that we defined at the beginning for getting all the countries that a given continent has.
In addition, we need to define a struct that matches the endpoint’s response and it will need to be a Decodable type.
Then, we just need to call our getData(from:)
function and pass it the endpoint for which we want to perform the request.
We now have an API manager that hides all the networking logic from the rest of the application.
If in the future, we need to call another endpoint, the only thing that we must do is define it inside our ApiEndpoint with its information (path, method, and parameters if any). No extra work is required.
As you may noticed, taking advantage of the async/await pattern within URLSession will improve your code readability and maintenance.
It’s true that there are some very powerful third-party libraries, like Alamofire, that solve all the networking communications for us. Nonetheless, if your needs are only making simple requests to the server, creating your own custom networking API might be the right choice.