agentsclimarketplace

Swift networking

Skill jeremieb/swift-unit-test-instructions/skills/swift-networking

A reusable framework of Claude Code instructions for Swift, SwiftUI, and UIKit projects.

Install
npx -y skills add jeremieb/swift-unit-test-instructions --skill swift-networking

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 5 stars5 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Builds a protocol-based async/await networking layer with URLSession, testable abstractions, error handling, and mock support for Swift projects. Use when user says "add networking", "create an API layer", "fetch data from API", "build a network service", "add URLSession", "implement HTTP requests", "create a REST client", or "set up API calls".

SKILL.md

8.8 KB, as published. Nobody here has run it

Swift 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

  • URLSession is hidden behind HTTPClientProtocol — never used directly in business code
  • ViewModels depend on repository protocols, not on APIClient directly
  • APIError is exhaustive and has user-readable descriptions
  • JSON decoding uses convertFromSnakeCase (or explicit CodingKeys)
  • Base URL is a constant — not hardcoded as a string literal in call sites
  • MockHTTPClient and Mock[X]Repository are 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.swift
  • APIError.swift
  • Endpoint.swift
  • APIClient.swift
  • ProductRepository.swift + ProductRepositoryProtocol
  • MockHTTPClient.swift
  • MockProductRepository.swift

Example 2: Adding a new endpoint

User says: "Add an endpoint to POST a new order"

Actions:

  1. Add createOrder(body: OrderRequest) to the endpoint enum or repository
  2. Add the method to OrderRepositoryProtocol and OrderRepository
  3. 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.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.