Skip to Content
Course content

52: Building a Simple Networking Layer

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When you first start hitting APIs in Swift, the temptation is to just throw a URLSession call right into your ViewModel or, heaven forbid, your ViewController. It feels efficient at the time. You just want the data on the screen, and URLSession.shared.dataTask is right there, ready to go. But as your app grows from one screen to ten, this "quick and dirty" approach becomes a maintenance nightmare.

The "Just Get It Working" Approach

Imagine we're building an app that tracks SpaceX launches. In a naive implementation, you might write a function in your ViewModel that looks something like this:

func fetchLaunches() async {
    let url = URL(string: "https://api.spacexdata.com/v4/launches")!
    do {
        let (data, _) = try await URLSession.shared.data(from: url)
        let launches = try JSONDecoder().decode([Launch].self, from: data)
        self.launches = launches
    } catch {
        print("Something went wrong: \(error)")
    }
}

On the surface, this works. It's concise. But I've seen this pattern kill productivity in larger projects because it mixes what the app is doing (fetching launches) with how it's doing it (URLSession, JSONDecoder, specific URLs). You're essentially hard-coding your networking logic into your business logic.

Where This Breaks

The moment you need to fetch "Latest Launch" or "Company Details," you'll find yourself copying and pasting that do-catch block over and over. If you suddenly need to add an API key to every request header, you're hunting through fifteen different files to update a string. Worse, testing becomes a chore. Since you're calling URLSession.shared directly, you can't easily swap the real network for a mock one during a unit test without some very ugly hacking.

Building a Scalable Client

The better way is to separate the definition of your API from the execution of the request. I usually start by creating an Endpoint abstraction. Instead of scattering strings everywhere, we use an enum to define our routes. This gives us a single place to manage URLs and HTTP methods.

enum SpaceXEndpoint {
    case launches
    case latestLaunch
    
    var url: URL {
        switch self {
        case .launches: return URL(string: "https://api.spacexdata.com/v4/launches")!
        case .latestLaunch: return URL(string: "https://api.spacexdata.com/v4/launches/latest")!
        }
    }
}

Now, we build a generic NetworkClient. The key here is using a generic type T: Decodable. I don't want my networking layer to know what a "Launch" is; I just want it to know how to fetch data and turn it into something that conforms to Decodable.

protocol NetworkClientProtocol {
    func request<T: Decodable>(_ endpoint: SpaceXEndpoint) async throws -> T
}

class NetworkClient: NetworkClientProtocol {
    func request<T: Decodable>(_ endpoint: SpaceXEndpoint) async throws -> T {
        let (data, response) = try await URLSession.shared.data(from: endpoint.url)
        
        guard let httpResponse = response as? HTTPURLResponse, 
              (200...299).contains(httpResponse.statusCode) else {
            throw URLError(.badServerResponse)
        }
        
        return try JSONDecoder().decode(T.self, from: data)
    }
}

By doing this, your ViewModel becomes blissfully ignorant of the networking plumbing. It just says, "Hey client, give me the launches," and it gets a typed array back. If you ever decide to switch from URLSession to something like Alamofire, or if the API version changes from v4 to v5, you only change one file, not your entire app. I highly recommend using the protocol NetworkClientProtocol here—it allows you to inject a MockNetworkClient during tests so you aren't burning through your API quota or relying on a stable internet connection to verify your UI logic.




📋 Practical Task

Implement a Generic Movie API Client

Build a networking layer for a hypothetical Movie Database API. You will need to implement the following:

  • An MovieEndpoint enum that handles two routes: .popularMovies and .movieDetails(id: Int). Ensure the details route dynamically inserts the ID into the URL string.
  • A MovieClient class that conforms to a protocol. It should feature a generic fetch<T: Decodable> method that handles the URLSession call and decodes the response.
  • A MovieViewModel that uses the MovieClient to fetch a list of movies. The ViewModel should not contain any URLSession or JSONDecoder code; it should rely entirely on the client.

Requirement: Use async/await for the asynchronous calls and ensure that non-200 HTTP status codes throw a custom error.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.