Member-only story
Build a General Purpose API Kit With Combine
No matter the remote host, HTTP method, or response type, this approach will serve any purpose your app may need

Introduction
One way or another, 99% of apps need to access data stored in a remote server, therefore making it essential to have an API client to handle the HTTP requests/responses. Now, although this data often comes from a sole source, it’s not unusual to have more than one. Imagine, for instance, that you wish to display weather information in your app by using AccuWeather or that you wish to implement a mobile-only feature by using Firebase.
Furthermore, this remote data might not always be a JSON file as we’re used to. It could be a JSON type… but also an image, a Passbook, XML, PDF, … or a simple plain text! In these scenarios, we tend to create a different Webservice/Repository/API Client
, but is this totally necessary?
In this tutorial, we’ll be implementing a general-purpose API client to unify all these approaches into one common solution. We’ll apply the following concepts:
- separation of concerns principle (SRP)
- Protocol extensions
- Generics
- First-class functions or functions as first-class citizens
The goal is to implement an elegant and easy-to-use API client that allows us to make any kind of HTTP request:
@Published var beers: [Beer] = []
@Published var beerPassbook: PKPass?
@Published var beersMenu: PDFDocument?func fetchData() {
apiClient
.send(BeersRequest())
.json()
.replaceError(with: [])
.assign(to: \.beers, on: self)
.store(in: &disposeBag) apiClient
.send(BeerPassbookRequest())
.passbook()
.replaceError(with: nil)
.assign(to: \.beerPassbook, on: self)
.store(in: &disposeBag) apiClient
.send(BeersMenuRequest())
.pdf()
.replaceError(with: nil)
.assign(to: \.beersMenu, on: self)
.store(in: &disposeBag)
}