Networking Architecture Recommendations

I'm putting together a networking layer for an iOS app. I'm planning on using URLSession and models conforming to Codable so I'll be avoiding 3rd party dependencies where I can.

The application will be consuming a number of endpoints across several API's, handling various request types, response types and payloads, so I'd like to find something that scales well but at the same time offers a simple abstraction within my services.

It's important it is testable too.

( Not asking for much I know :joy: )

I'm thinking I need some sort of generic API client I can conform too, a protocol for an endpoint and some enums I can use to provide the shape of each endpoint.

I imagine the call from a service looking something like this

    apiClient(service: .profile, endpoint: .fetchByUserId(userId: "123")) { response in
        switch response {
            case .success(let profile):
                // do something
            case .failure(let error):
                // do something
        }
    }

I'd love to hear of any resources or advice for building such an abstraction if anyone can help please?

Check out this talk from WWDC. It has some good advice on that topic you might find helpful.

1 Like

I know you’re tying to avoid 3rd party depdencies, but I would recommend checking out Moya, even if you just reference its architecture ideas.

It’s pattern of calls as enums is exactly what you had in mind, and from experience leads to really nice maintainable, consise code.

1 Like

I like the approach taken by the objc.io guys, see their videos about networking stack design.

Generally I don’t like designs where a request does something directly. I like when a request is a value that can be inspected, transformed or loaded to yield a response, not an action that is executed immediately. Something along this:

struct Request<T> {
    let encode: (Void) throws -> URLRequest
    let decode: (Data) throws -> T
    // Plus other properties like path, content type, etc.
}

And then there’s a thin extension on URLSession to load requests:

extension URLSession {
    // Async version
    func load<T>(_ request: Request<T>, completion: @escaping (Result<T, Error>) -> Void) { … }
    // Sync version for testing or playgrounds
    func load<T>(_ request: Request<T>) throws -> T { … }
}

Then I have a multitude of convenience initializers on Request to support various scenarios such as Codable requests and responses, different HTTP methods and so on. An API can then be modelled like this:

struct MyAPI {
    func createAccount(name: String) -> Request<Account> {
        // Local payload type
        struct Payload: Codable {
            let name: String
        }
        // T is inferred to be Account, will be automatically decoded from JSON
        return Request(path: "/user", json: Payload(name: name))
    }
    func deleteAccount(id: Int) -> Request<Void> { … }
}

So the final usage looks like this:

// Methods on MyAPI can be static or you can use the MyAPI instance to store credentials or server URLs
let service = MyAPI()
let accountCreationRequest = service.createAccount(name: "Bimbo")
let account = try? URLSession.shared.loadSync(accountCreationRequest)

And testing is trivial:

let service = MyAPI()
let accountCreationRequest = service.createAccount(name: "Bimbo")
let request = try? accountCreationRequest.encode()
XCTAssert(…)
1 Like

Some awesome replies, thanks everyone :+1: