Tips for consuming APIs
6 min read
Last time I wrote a few tips for writing APIs; this time I'd like to write a few for consuming them.
- The most important one I can give is to always have a layer between your app and the network; this will ensure you always have a single source of truth:
- create models, no matter how few fields an object has, instead of reading from dictionaries/arrays all over the app. This will make adding/changing/removing fields a breeze if required, will ensure everything is type safe, and that you don't have to worry about typos every time you use a field; autocomplete is your friend;
- have your own "manager" for any third party services. For example, don't call Google Analytics directly, but your own
Analyticsclass, which in turn calls that. This way, if you ever need to switch services, there's just one place to do the changes; - create an API object, preferably with
enumsfor your endpoints and/or staging(s); it's much easier to reason with:
struct API {
static let baseURL = URL(string: "https://rolandleth.com")!
enum Endpoint: String {
case sessions
case followers
var url: URL {
return API.baseURL.appendingPathComponent(rawValue)
}
}
enum Environment: Int {
case production
case staging1
case staging2
static var current: Environment {
let raw = UserDefaults.current.integer(forKey: "currentEnvironment")
return Environment(rawValue: raw) ?? .production
}
}
func userInfo(completion: @escaping ([String: Any]) -> Void) {
var request = URLRequest(url: Endpoint.sessions.url)
request.httpMethod = "GET"
// ...
}
func login(email: String, password: String, completion: @escaping ([String: Any]) -> Void) {
var request = URLRequest(url: Endpoint.sessions.url)
request.httpMethod = "PUT"
// ...
}
func followers(completion: @escaping (Int) -> Void) {
// For stagings we want to easily test this feature,
// so we just return a random number up to 200.
guard Environment.current == .production else {
completion(arc4random() % 200)
return
}
// ...
}
}- Be consistent:
- if one object has a property
startDate, don't have other objects usestartingDate; - if some
Boolsuse theis/hasnomenclature, use it everywhere. For example, don't have some flagsisAvailableandhasExpirationDate, but othersavailableandexpirationDatePresent; - if one object has a child object/wraps some keys in an object for better structure, do that everywhere else it applies as well:
- if one object has a property
struct Address {
let city: String
let street: String
}
struct User {
let name: String
let address: Address
}
struct Event {
let name: String
let address: Address
}
// Don't:
struct Event {
let name: String
let city: String
let street: String
}
/*
Even if the json looks like this:
{
"name": "Geneva International Motor Show",
"city": "Geneva",
"street": "Route François-Peyrot 30"
}
Just wrap the keys into the proper object yourself,
or refer your backend to my previous post about writing APIs :)
*/- Make use of
enums. If a field can have a finite set of values,enumscan make your life a bit easier, by providing type safety and autocompletion:
struct User {
let name: String
let address: Adress
let role: Role
}
struct Role: String {
case guest
case sales
case financial
case administrator = "admin"
}
// [...]
if user.role == .guest { /* do something */ }
else if user.role == .administrator { /* do something else */ }
// vs having role be a String
if user.role == "guest" { /* do something */ }
else if user.role == "admin" { /* do something else */ }
// or worse, if the API was designed in a weird way
if user.role == "g" { /* do something */ }
else if user.role == "a" { /* do something else */ }