A small async/await HTTP client for Swift, built entirely on URLSession and Foundation. No third-party dependencies, works on iOS and macOS.
Licensed under MIT.
It ships two library targets:
- HTTPClientKit — the
HTTPClientprotocol, anURLSession-backed implementation (URLSessionHTTPClient), and typedget/post/put/patch/deletehelpers that encode/decode JSON for you. - HTTPClientKitMock —
MockHTTPClient, a test double that records every request it receives and answers with a fixed response, a fixed error, or a per-request closure.
.package(url: "https://github.com/PedroAugusto01/HTTPClientKit", from: "1.0.0").product(name: "HTTPClientKit", package: "HTTPClientKit"),
.product(name: "HTTPClientKitMock", package: "HTTPClientKit"), // test targets onlyRequires iOS 15+ / macOS 12+ (uses URLSession's async data(for:)).
public protocol HTTPClient: Sendable {
func send(_ request: HTTPRequest) async throws -> HTTPResponse
}HTTPRequest is transport-agnostic: path (resolved against the client's baseURL), method, queryItems, headers, and an optional body: Data. HTTPResponse carries back statusCode, body, and headers. Everything else in the package is built on top of this one method.
get/post/put/patch/delete are default implementations on HTTPClient, so any conforming type gets them for free:
let client = URLSessionHTTPClient(baseURL: URL(string: "https://api.example.com")!)
let user: User = try await client.get("users/42")
let created: Post = try await client.post("posts", body: NewPost(title: "Hello"))They call send(_:), classify the status code, decode the body with JSONDecoder/encode the request with JSONEncoder, and throw an HTTPClientError on failure:
public enum HTTPClientError: Error, Equatable, Sendable {
case invalidURL
case invalidResponse
case timeout
case cancelled
case transport(String) // any other URLSession failure
case encodingFailed(String)
case decodingFailed(String)
case clientError(statusCode: Int, body: Data?) // 4xx
case serverError(statusCode: Int, body: Data?) // 5xx
}Need something the JSON helpers don't cover (multipart, a non-2xx-but-not-an-error status, custom decoding)? Call send(_:) directly and inspect the raw HTTPResponse yourself.
URLSessionHTTPClient(baseURL:session:defaultHeaders:) takes headers applied to every request (e.g. Authorization, Accept-Language) — per-request headers passed to get/post/etc. are merged on top and win on conflicts.
import HTTPClientKit
import HTTPClientKitMock
func testFetchesUser() async throws {
let client = MockHTTPClient(statusCode: 200, body: try JSONEncoder().encode(User(id: 42)))
let repository = UserRepository(client: client)
let user = try await repository.fetchUser(id: 42)
XCTAssertEqual(user.id, 42)
XCTAssertEqual(client.receivedRequests.first?.path, "users/42")
}For responses that depend on the request (different status per path, sequential calls, etc), use the closure initializer:
let client = MockHTTPClient { request in
request.path == "users/1"
? HTTPResponse(statusCode: 200, body: userJSON)
: HTTPResponse(statusCode: 404)
}Or stub a thrown error directly:
let client = MockHTTPClient(error: HTTPClientError.timeout)UserRepository only needs to depend on the HTTPClient protocol, never on URLSessionHTTPClient or MockHTTPClient directly — that's what makes the swap possible.
Sources/HTTPClientKitDemo is a small command-line executable that hits the free jsonplaceholder test API to show a real GET and POST round trip:
swift run HTTPClientKitDemoswift testURLSessionHTTPClientTests exercises the real URLSession-backed client against a stubbed URLProtocol, so the suite never touches the network.