Uncategorized

What makes a good network layer

Here’s what I want in a networking layer:

  1. Should be easy to switch between different hosts and API versions while keeping all the endpoints the same
  2. Should be possible to separate the logic for setting up a network call and firing it
  3. Should be easy to mock server responses
  4. Should be easy to separate concerns when handling responses
  5. Should have good default error handling

Let’s talk about these.

Switching Hosts

Being able to simply switch hosts is really important, but not generally difficult. Lots of projects have some code that looks something like:

let prodHost = "elliotschrock.com"
let stagingHost = "staging.elliotschrock.com"
let devHost = "localhost:3000"
let host = prodHost
let apiV1 = "v1"
let apiV2 = "v2"
let apiVersion = apiV1
let secureScheme = "https"
let insecureScheme = "http"
let scheme = secureScheme
let baseUrl = "\(scheme)://\(host)/api/\(apiVersion)/"

And that’s easy enough to configure. Any time you want to switch servers, just change the variable that host points to.

However, this approach does cause some difficulties. On the one hand, it’s great that we’re separating the scheme from the host from the apiVersion, allowing you to change each of them independently. On the other hand, there is some dependency between those; your localhost probably won’t have HTTPS enabled, and you might be working on v2 of your API on dev/staging while prod remains on API v1. Additionally, what happens when some of your endpoints need API v1 while others require API v2? What happens when those things change based on the host, i.e. you’re implementing a second batch of v2 endpoints on localhost that haven’t been pushed to staging yet?

Second, it’s pretty project specific. You’re pretty tightly coupled to the strings as defined, and you’re likely to use baseUrl all over the place. You wouldn’t really want to change those from lets to vars, since having them as constants keeps them from changing at run time, which is good… but if they’re top level lets then you can’t accept them as inputs to your system; they’d have to be defined and available at compile time. Without creating an object to hold them all, it’s hard to imagine building a library off of it.

But if you did create a server configuration object which couples the scheme, host, and API version, you could easily switch between the different configurations without worrying you’ve forgotten to change one of the components:

struct ServerConfig {
  let scheme: String = "https"
  let host: String
  let apiVersion: String = "api/v1"
}
let prodServer = ServerConfig(host: "elliotschrock.com")
let stagingV1 = ServerConfig(host: "staging.elliotschrock.com")
let stagingV2 = ServerConfig(host: "staging.elliotschrock.com", 
                             basePath: "api/v2")
let devV1 = ServerConfig(scheme: "http", host: "localhost:3000")
let devV2 = ServerConfig(scheme: "http", host: "localhost:3000",
                         basePath: "api/v2")

Next you could create a type for representing a specific endpoint, independent from the server. Something like:

struct Endpoint {
    var httpMethod: String = "GET"
    var httpHeaders: [String: String] = [:]
    var getParams: [String: Any] = [:]
    var path: String
    var postData: Data? = nil
}

Those properties, generally, are the types of things we configure on an endpoint specific basis. For instance, usually a given endpoint doesn’t change the httpMethod based on if it’s staging or prod, and the path doesn’t usually change based on if you’re using “https” or not; so, those things can be stored separately, and combined when you need to create a URLRequest. All you need to do then is take the information from the ServerConfig and merge it with the Endpoint specific data.

Separate Configuring from Firing

Network calls are ubiquitous in app development. I often find, however, that I don’t always want to configure and fire a network call in the same object. Refreshing some server data is common app functionality, but triggering a network refresh doesn’t rely on what type of data the server returns, or really which endpoint is being refreshed at all. Similarly, paginated server responses have a common structure to them, but don’t depend on what is actually being paginated.

So it’d be convenient if you could make something that gets configured in one place but fired off somewhere else. What I often do in my code is make my network calls implement a protocol called Fireable, which has one method: fire. That way I can setup a call in one place, pass it to a totally new place, and use call.fire() to start it at a later time.

To make this convenient for mocking (see below), I implement the class something like this:

class NetCall: Fireable {
  var firingFunction: (NetCall) -> Void = createRequest(_:)
  ... other stuff ...
  func fire() {
    firingFunction(self)
  }
}

so that I can easily swap out what happens when fire is called for different functionality.

Separating Concerns, Mocking, and Error Handling

The object that configures a network call needn’t be the same the object that uses the data which the server returns. Decoupling these concerns makes it so much easier to modularize your code.

For instance, a table view doesn’t need to know whence came the data it displays. An array of models that backs that table view could be populated from any number of places, be it server, database, mocks, a combination of those, or something completely different. Even a JSONDecoder object doesn’t need to know it came from a server rather than disk. So parsing, transforming, and displaying could (should?) rightly belong in unrelated classes. Our network layer should support that, so that each bit can be tested independently.

So, you might set up a separate object that provides hooks for handling aspects of the server’s response independently. Something like:

struct NetworkResponder {
    var taskHandler: (URLSessionDataTask?) -> Void = { _ in }
    var responseHandler: (URLResponse?) -> Void = { _ in }
    var httpResponseHandler: (HTTPURLResponse) -> Void = { _ in }
    var dataHandler: (Data?) -> Void = { _ in }
    var errorHandler: (NSError) -> Void = { _ in }
    var serverErrorHandler: (NSError) -> Void = { _ in }
    var errorDataHandler: (Data?) -> Void = { _ in }
    
    public init() {}
}

Which you could use in a data task completion block like so:

  responder.responseHandler(response)
  if let e = error as NSError? {
      responder.errorHandler(e)
      responder.errorDataHandler(data)
  } else if let httpResponse = response as? HTTPURLResponse {
      responder.httpResponseHandler(httpResponse)
      if httpResponse.statusCode > 299 {
          if let data = data {
              responder.errorDataHandler(data)
          }
          responder.serverErrorHandler(NSError(domain: "Server", code: httpResponse.statusCode, userInfo: ["url" : httpResponse.url?.absoluteString as Any]))
      } else {
          responder.dataHandler(data)
      }
  }

Then, the object that parses the data and passes it to the view could be different from the object that stops the spinner from spinning, or the object that handles server errors. For example:

var rspdr = NetworkResponder()
rspdr.dataHandler = viewModel.parseAndDisplay(_:)
rspdr.responseHandler = tableView.refreshControl.endRefreshing
rspdr.serverErrorHandler = vc.displayAlertForError(_:)

All those live in different places, and are properly separated concerns.

Mocking

There are lots of libraries out there that allow you to mock server responses. In fact, Apple supplies hooks for us to do so, which OHHTTPStubs, Nocilla, and others use. However, they can require a fair amount of setup, and may include libraries that you don’t want to include in your release target. If you want to be able to navigate your app offline using mocked data, that can be a conundrum: you don’t want to include OHHTTPStubs in your app target, but otherwise you won’t be able to intercept the offline network calls and stub a response.

However, being able to separate concerns as described in the last section makes it easy to mock responses: indeed, there’s a function right there named dataHandler! So, when the call gets fired, we need a way to just pass mocked data to our dataHandler instead of actually firing the call. What’s a good way to do that?

The way I generally do it is by using principles from Functional Programming. Let’s take a look at how we implemented Fireable:

class NetCall: Fireable {
  var firingFunction: (NetCall) -> Void = createRequest(_:)
  ... other stuff ...
  func fire() {
    firingFunction(self)
  }
}

The “other stuff” in this case, would include storing a network responder in a property on NetCall. That way, when we want to mock a response, we can just replace firingFunction with a closure that looks like this:

{ call in
  let mockData = ...
  call.responder.dataHandler(mockData)
}

Wow that was easy! And it didn’t require any stubbing specific libraries at all.

Error Handling

This, too, is made simpler with how we’ve defined our network responder. All we have to do to handle errors is pass a function to either errorHandler or serverErrorHandler, depending on which type of error we want to react to.

Because our data task completion handler, as defined above, automatically creates NSErrors with the server response code when present, we can even generalize our error handlers. We might create an extension on UIViewController which, based on the response code, displays a UIAlertController filled in from a list of generic error messages. Some examples might include: “Not found” for a 404, “Error: server crashed” for a 500, or “Invalid data sent to server” for a 422. Making that easier saves hours of work and makes your app more professional by default.

FunNet

I’ve made a networking library based on these principles, with extensions for Combine and ReactiveSwift. Any implementation of NetworkCall, whether for ReactiveSwift, Combine, or purely functional, is a combination of three things: a server configuration, an endpoint, and a responder. When fire is called on it, it passes itself to a firing function property, which defaults to generating a URLRequest from the server config + endpoint and then passing the server’s response to the responder (with almost identical code to the data task completion handler above).

FunNet has made it significantly easier for me to build libraries that rely on network calls that they don’t create or configure. I’ve created classes that, given a network call and a table view, will automatically handle paging and refreshing the call based on user behavior (e.g. pulling down to refresh or scrolling to the bottom to page). It’s also enabled creating entire login/splash/registration/password reset flows with no knowledge about what the api calls look like. This saves me tons of time whenever I start a new project that requires user auth.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s