swift-networking
npx machina-cli add skill jeremieb/swift-unit-test-instructions/swift-networking --openclawSwift Networking Layer
Instructions
Step 1: Clarify Requirements
Ask if not already provided:
- Base URL (can be a placeholder constant)
- Authentication method: Bearer token, API key in header, OAuth, none
- Endpoints to implement upfront (or start with the pattern only)
- Response format: JSON (assumed), XML, or other
- Error handling needs: retry logic, token refresh, offline support
Step 2: Generate the Core Layer
Generate in Services/Networking/:
HTTPClientProtocol — the testability key:
// Services/Networking/HTTPClientProtocol.swift
import Foundation
protocol HTTPClientProtocol {
func data(for request: URLRequest) async throws -> (Data, URLResponse)
}
// URLSession conforms for free — no wrapper needed
extension URLSession: HTTPClientProtocol {}
APIError:
// Services/Networking/APIError.swift
import Foundation
enum APIError: LocalizedError {
case invalidURL
case invalidResponse(statusCode: Int)
case decodingError(Error)
case unauthorized
case noInternetConnection
case unknown(Error)
var errorDescription: String? {
switch self {
case .invalidURL: return "Invalid URL."
case .invalidResponse(let code): return "Server error (\(code))."
case .decodingError: return "Failed to parse server response."
case .unauthorized: return "You are not authorized. Please log in again."
case .noInternetConnection: return "No internet connection."
case .unknown(let error): return error.localizedDescription
}
}
}
Endpoint — type-safe request building:
// Services/Networking/Endpoint.swift
import Foundation
struct Endpoint {
let path: String
let method: HTTPMethod
let queryItems: [URLQueryItem]?
let body: Encodable?
let headers: [String: String]
init(
path: String,
method: HTTPMethod = .get,
queryItems: [URLQueryItem]? = nil,
body: Encodable? = nil,
headers: [String: String] = [:]
) {
self.path = path
self.method = method
self.queryItems = queryItems
self.body = body
self.headers = headers
}
func urlRequest(baseURL: URL, authToken: String? = nil) throws -> URLRequest {
var components = URLComponents(url: baseURL.appendingPathComponent(path), resolvingAgainstBaseURL: true)
components?.queryItems = queryItems
guard let url = components?.url else { throw APIError.invalidURL }
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
if let token = authToken {
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
if let body = body {
request.httpBody = try JSONEncoder().encode(body)
}
return request
}
}
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case patch = "PATCH"
case delete = "DELETE"
}
APIClient:
// Services/Networking/APIClient.swift
import Foundation
final class APIClient {
private let httpClient: HTTPClientProtocol
private let baseURL: URL
private let decoder: JSONDecoder
init(
httpClient: HTTPClientProtocol = URLSession.shared,
baseURL: URL
) {
self.httpClient = httpClient
self.baseURL = baseURL
self.decoder = JSONDecoder()
self.decoder.keyDecodingStrategy = .convertFromSnakeCase
self.decoder.dateDecodingStrategy = .iso8601
}
func fetch<T: Decodable>(_ endpoint: Endpoint, authToken: String? = nil) async throws -> T {
let request = try endpoint.urlRequest(baseURL: baseURL, authToken: authToken)
let (data, response) = try await httpClient.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw APIError.invalidResponse(statusCode: -1)
}
switch httpResponse.statusCode {
case 200...299:
do {
return try decoder.decode(T.self, from: data)
} catch {
throw APIError.decodingError(error)
}
case 401:
throw APIError.unauthorized
default:
throw APIError.invalidResponse(statusCode: httpResponse.statusCode)
}
}
func send(_ endpoint: Endpoint, authToken: String? = nil) async throws {
let request = try endpoint.urlRequest(baseURL: baseURL, authToken: authToken)
let (_, response) = try await httpClient.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw APIError.invalidResponse(statusCode: (response as? HTTPURLResponse)?.statusCode ?? -1)
}
}
}
Step 3: Generate a Repository (Service Layer)
ViewModels should never call APIClient directly — wrap it in a repository:
// Services/Networking/UserRepository.swift
import Foundation
protocol UserRepositoryProtocol {
func fetchUsers() async throws -> [User]
func fetchUser(id: String) async throws -> User
}
final class UserRepository: UserRepositoryProtocol {
private let apiClient: APIClient
init(apiClient: APIClient) {
self.apiClient = apiClient
}
func fetchUsers() async throws -> [User] {
try await apiClient.fetch(Endpoint(path: "/users"))
}
func fetchUser(id: String) async throws -> User {
try await apiClient.fetch(Endpoint(path: "/users/\(id)"))
}
}
Step 4: Generate Mock for Testing
// Always generate alongside the real implementation
final class MockHTTPClient: HTTPClientProtocol {
var stubbedData: Data = Data()
var stubbedResponse: URLResponse = HTTPURLResponse(
url: URL(string: "https://example.com")!,
statusCode: 200,
httpVersion: nil,
headerFields: nil
)!
var stubbedError: Error?
private(set) var requestsMade: [URLRequest] = []
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
requestsMade.append(request)
if let error = stubbedError { throw error }
return (stubbedData, stubbedResponse)
}
}
final class MockUserRepository: UserRepositoryProtocol {
var stubbedUsers: [User] = []
var stubbedError: Error?
func fetchUsers() async throws -> [User] {
if let error = stubbedError { throw error }
return stubbedUsers
}
func fetchUser(id: String) async throws -> User {
if let error = stubbedError { throw error }
return stubbedUsers.first { $0.id == id } ?? stubbedUsers[0]
}
}
Step 5: Quality Checklist
-
URLSessionis hidden behindHTTPClientProtocol— never used directly in business code - ViewModels depend on repository protocols, not on
APIClientdirectly -
APIErroris exhaustive and has user-readable descriptions - JSON decoding uses
convertFromSnakeCase(or explicitCodingKeys) - Base URL is a constant — not hardcoded as a string literal in call sites
-
MockHTTPClientandMock[X]Repositoryare generated alongside the real types - No network calls anywhere in the View layer
Examples
Example 1: REST API with auth
User says: "Add networking to fetch a list of products from our REST API at api.myapp.com"
Files generated:
HTTPClientProtocol.swiftAPIError.swiftEndpoint.swiftAPIClient.swiftProductRepository.swift+ProductRepositoryProtocolMockHTTPClient.swiftMockProductRepository.swift
Example 2: Adding a new endpoint
User says: "Add an endpoint to POST a new order"
Actions:
- Add
createOrder(body: OrderRequest)to the endpoint enum or repository - Add the method to
OrderRepositoryProtocolandOrderRepository - Update
MockOrderRepository
Troubleshooting
Decoding fails silently: Add error logging in the catch block of fetch(). Print the raw JSON string for debugging: String(data: data, encoding: .utf8).
401 errors in tests: MockHTTPClient is returning the wrong status code. Set stubbedResponse with statusCode: 200 explicitly.
Tests are hitting real network: URLSession.shared is still used directly somewhere. Verify the APIClient is initialized with MockHTTPClient in tests.
Source
git clone https://github.com/jeremieb/swift-unit-test-instructions/blob/main/skills/swift-networking/SKILL.mdView on GitHub Overview
Swift Networking Layer builds a protocol-based, testable networking stack using async/await and URLSession. It emphasizes type-safe Endpoint construction, robust APIError handling, and mock support for unit tests, enabling reliable API interactions in Swift projects.
How This Skill Works
Define HTTPClientProtocol to enable testability; URLSession conforms automatically. Endpoint builds a type-safe URLRequest with baseURL, headers, and optional body. APIClient uses the httpClient and baseURL to perform requests asynchronously, decode responses, and map failures to APIError.
When to Use It
- Add networking capabilities to a Swift app
- Create a type-safe API layer for REST calls
- Fetch data from an API with async/await
- Build a reusable network service with mock support
- Set up authenticated HTTP requests using Bearer tokens
Quick Start
- Step 1: Draft baseURL, authentication strategy, and endpoints to implement
- Step 2: Implement core layer: HTTPClientProtocol, APIError, Endpoint, APIClient
- Step 3: Use in app code or write tests with mocks to exercise API calls
Best Practices
- Use HTTPClientProtocol to enable swapping in mocks for testing
- Inject httpClient and baseURL rather than using singletons
- Centralize error handling with a well-defined APIError
- Create type-safe Endpoint instances to enforce method, headers, and body
- Write unit tests with mocks to cover success and failure scenarios
Example Use Cases
- Fetch a list of users from /users using GET and decode to Swift models
- Post a new item to /items with a JSON body and handle response
- Make authenticated requests by supplying a Bearer token in headers
- Decode JSON responses into strongly-typed Swift structs
- Mock network responses in unit tests to verify APIClient behavior