Tips for consuming APIs

:  ~ 2 min read

Last time I wrote a few tips for writing APIs; this time I'd like to write a few for consuming them.

  1. 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 Analytics class, 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 enums for 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
    }
    // ...
  }
}
  1. Be consistent:
    • if one object has a property startDate, don't have other objects use startingDate;
    • if some Bools use the is/has nomenclature, use it everywhere. For example, don't have some flags isAvailable and hasExpirationDate, but others available and expirationDatePresent;
    • if one object has a child object/wraps some keys in an object for better structure, do that everywhere else it applies as well:
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 :)
*/
  1. Make use of enums. If a field can have a finite set of values, enums can 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 */ }