A lightweight, modern, and robust REST API client written in Swift.
Fetcher is a Swift library designed to simplify interactions with RESTful APIs. It provides a clean, protocol-oriented, and testable solution for making network requests, handling responses, and managing common API workflows like authentication and error handling.
The core of the framework is the Fetcher struct, which acts as the main client for performing requests. It operates on objects conforming to the APIURL protocol, which provides a structured and type-safe way to define your API endpoints, avoiding scattered URL strings across your codebase.
Key features include:
- Asynchronous API: Modern
async/awaitsyntax for clean and concurrent network calls. - Protocol-Driven Endpoints: Define your API endpoints semantically using the
APIURLprotocol. - Robust Error Handling: A comprehensive
APIErrorenum to represent various network and server errors. - Token Management: Built-in logic to handle token-based authentication, including automatic retries and token refresh.
- Environment-Based Configuration: Use the
FetcherEnvironmentprotocol to inject dependencies likeURLSessionand custom logic, making your networking layer highly testable. - Concurrency Utilities: Helpers for running multiple requests in parallel.
For more information visit Documentation.
Add Fetcher as a dependency to your Package.swift file:
.package(url: "https://github.com/Snapp-Mobile/Fetcher.git", from: "0.1.0")To use Fetcher, you need to provide an implementation of FetcherEnvironment.
import Fetcher
import Foundation
import os.log
// A basic implementation of FetcherEnvironment for demonstration.
// In a real application, you would manage tokens, network reachability,
// and logging more robustly.
class MyProductionEnvironment: FetcherEnvironment {
let urlSession: URLSession = .shared
var isNetworkingAvailable: Bool = true // You might use Network.framework here
var apiErrorsLogger: FetcherLogger? = nil // Implement a custom logger if needed
// Placeholder implementations for token management
func updateToken(to newToken: Fetcher.Token?, logger: FetcherLogger?) async throws -> Fetcher.Token? {
// ... update stored token ...
return newToken
}
func refreshToken(_ token: Fetcher.Token?, using fetcher: Fetcher) async throws -> Fetcher.Token? {
// ... logic to refresh token, e.g., make an API call to your auth server ...
return token // Return a new, valid token
}
func getToken(logger: FetcherLogger?) async throws -> Fetcher.Token? {
// ... retrieve current token from storage ...
return nil
}
func logout() async throws {
// ... clear stored token and user session ...
}
}
let myEnvironment = MyProductionEnvironment()
let fetcher = Fetcher(environment: myEnvironment)Define your API endpoints by conforming to the APIURL protocol:
import Fetcher
import Foundation
// Example API endpoint definition
enum MyAPIEndpoint: APIURL {
case getGreeting
var path: String {
switch self {
case .getGreeting:
return "/greeting"
}
}
var requestMethod: String {
switch self {
case .getGreeting:
return "GET"
}
}
var url: URL? {
return URL(string: "https://api.example.com")?.appendingPathComponent(path)
}
func bodyParams(token: Fetcher.Token?) -> [String: Any]? {
return nil
}
func request(token: Fetcher.Token?) -> URLRequest? {
guard let url = self.url else { return nil }
var request = URLRequest(url: url)
request.httpMethod = requestMethod
// Add authorization header if token is available
if let accessToken = token?.accessToken {
request.addValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
}
return request
}
}
// Define a Decodable struct for your API response
struct GreetingResponse: Decodable {
let message: String
}
// To fetch data from your API:
func fetchGreeting() async {
do {
let greeting: GreetingResponse = try await fetcher.fetch(MyAPIEndpoint.getGreeting)
print("Received greeting: \(greeting.message)")
} catch {
print("Failed to fetch greeting: \(error.localizedDescription)")
}
}
// Call the function (e.g., from an async context)
// Task {
// await fetchGreeting()
// }