Swift API Client Strategy
This document outlines a comprehensive strategy for creating an API client in Swift using async/await
. The approach is designed to be modular, reusable, and easy to extend for various API endpoints.
Components
- Endpoint Protocol: Defines the base URL and path for each API endpoint.
- NetworkManaging Protocol: Defines the contract for network operations.
- NetworkManager Class: Implements the NetworkManaging protocol to handle network requests.
- Error Handling: Manages errors that may occur during network requests.
Implementation
1. Endpoint Protocol
Define a protocol to represent API endpoints. Each endpoint will have a base URL and a path.
protocol Endpoint {
var base: String { get }
var path: String { get }
}
2. NetworkManaging Protocol
Define a protocol for network operations.
protocol NetworkManaging {
func fetch<T: Decodable>(from endpoint: Endpoint) async throws -> T
}
3. NetworkManager Class
Implement the NetworkManaging protocol in the NetworkManager class.
final class NetworkManager: NetworkManaging {
static let shared = NetworkManager()
private let session: URLSession
private init(session: URLSession = .shared) {
self.session = session
}
func fetch<T: Decodable>(from endpoint: Endpoint) async throws -> T {
let request = try endpoint.urlRequest()
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.invalidResponse
}
try validateResponse(httpResponse)
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
throw NetworkError.decodingFailed
}
}
private func validateResponse(_ response: HTTPURLResponse) throws {
switch response.statusCode {
case 200...299:
return
case 400...499:
throw NetworkError.clientError(response.statusCode)
case 500...599:
throw NetworkError.serverError(response.statusCode)
default:
throw NetworkError.unknownError(response.statusCode)
}
}
}
enum NetworkError: Error {
case invalidResponse
case decodingFailed
case clientError(Int)
case serverError(Int)
case unknownError(Int)
}
6. Example Usage
Here’s an example of how to use the API client with a specific endpoint and data model.
struct ExampleEndpoint: Endpoint {
var base: String { return "https://api.example.com" }
var path: String { return "/data" }
}
struct ExampleData: Decodable {
let id: Int
let name: String
}
extension Request where Response == ExampleData {
static func fetchData() -> Self {
Request(
url: URL(string: "https://api.example.com/data")!,
method: .get([])
)
}
}
let request: Request<ExampleData> = .fetchData()
let response = try await URLSession.shared.decode(request)
This strategy provides a flexible and reusable framework for building API clients in Swift. You can extend it by adding more specific endpoints and data models as needed.
Case Study: Gutendex API
In this case study, we will apply the API client strategy to the Gutendex API, a JSON web API for Project Gutenberg ebook metadata.
Components
- Endpoint Protocol: Define the base URL and path for the Gutendex API.
- HttpMethod Enum: Use the existing HttpMethod enum to represent HTTP methods.
- Request Struct: Create requests for the Gutendex API endpoints.
- URLSession Extension: Use the existing URLSession extension for type-safe decoding.
- Error Handling: Handle potential errors during network requests.
Implementation
1. Endpoint Protocol
Define the Gutendex API endpoint.
struct GutendexEndpoint: Endpoint {
var base: String { return "https://gutendex.com" }
var path: String { return "/books" }
}
2. Request Struct
Create a request for fetching a list of books.
extension Request where Response == BookList {
static func fetchBooks(queryItems: [URLQueryItem] = []) -> Self {
let url = URL(string: "https://gutendex.com/books")!
return Request(url: url, method: .get(queryItems))
}
}
3. Data Models
Define the data models for the Gutendex API response.
struct BookList: Decodable {
let count: Int
let next: String?
let previous: String?
let results: [Book]
}
struct Book: Decodable {
let id: Int
let title: String
let subjects: [String]
let authors: [Person]
let translators: [Person]
let bookshelves: [String]
let languages: [String]
let copyright: Bool?
let mediaType: String
let formats: [String: String]
let downloadCount: Int
}
struct Person: Decodable {
let birthYear: Int?
let deathYear: Int?
let name: String
}
4. Example Usage
Fetch a list of books using the Gutendex API client.
let request: Request<BookList> = .fetchBooks(queryItems: [URLQueryItem(name: "languages", value: "en")])
let bookList = try await URLSession.shared.decode(request)
print(bookList.results)
This case study demonstrates how to apply the API client strategy to a real-world API, providing a structured and reusable approach to interacting with the Gutendex API.