A tiny, type-safe, dependency-free HTTP request builder for Swift.
NetworkRequest describes an HTTP request and how to parse its response as a
single, immutable value. It does not execute itself — URLSession,
async/await, Combine, or any mock you like is in charge of dispatch.
The library just gives you a strongly-typed, composable description of the
work.
import NetworkRequest
struct User: Decodable, Sendable {
let id: Int
let name: String
}
struct APIError: Decodable, Error, Sendable {
let message: String
}
let me = NetworkRequest<User, APIError>(
url: URL(string: "https://api.example.com/me"),
additionalHeaderFields: ["Authorization": "Bearer \(token)"]
)
let (data, response) = try await URLSession.shared.data(for: me.urlRequest())
let user = try me.parse(data, response)A non-2xx response decodes the body as APIError and throws it — your
do/catch block sees a typed error.
- Type-safe. A
NetworkRequest<User, APIError>carries both the success and the failure shape in its type. - Execution-agnostic. No baked-in transport. Use
URLSession, mock it, swap it for Combine, plug in your own. - Sendable-clean. Builds cleanly under Swift 6 strict concurrency and
conditionally conforms to
Sendablewhen its generic parameters do. - Zero dependencies. Just Foundation.
- Built-in cURL debugging.
request.cURLCommandreproduces a request on the command line.
Add the package to your Package.swift:
dependencies: [
.package(url: "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/MarvinNazari/NetworkRequest", from: "1.0.0"),
],
targets: [
.target(name: "MyApp", dependencies: ["NetworkRequest"]),
]In Xcode: File ▸ Add Package Dependencies… and paste the URL.
The package ships a second product, NetworkRequestTesting, so you can
unit test request building and response parsing without touching the
network. Add it to your test target only:
.testTarget(
name: "MyAppTests",
dependencies: ["MyApp", .product(name: "NetworkRequestTesting", package: "NetworkRequest")]
)It adds exactly three types — StubURLProtocol, RequestRecorder and
RecordedRequest — and no extensions on NetworkRequest, URLRequest,
URLSession or any other type it does not own.
StubURLProtocol builds an isolated URLSession that answers every request
through your handler. Sending a request is the same two lines as in
production code, because the core already exposes both closures:
import Testing
import NetworkRequest
import NetworkRequestTesting
@Test func fetchesTheCurrentUser() async throws {
let recorder = RequestRecorder()
let session = StubURLProtocol.session { request in
await recorder.record(request)
return .response(.json(#"{"id":1,"name":"Ada"}"#))
}
let (data, response) = try await session.data(for: try me.urlRequest())
let user = try me.parse(data, response)
#expect(user.name == "Ada")
let sent = try #require(await recorder.last)
#expect(sent.path == "/me")
#expect(sent.queryDictionary == ["expand": "profile"])
#expect(sent.headers["Authorization"] == "Bearer \(token)")
}Also included:
StubURLProtocol.session(returning:)— same response every time.StubURLProtocol.session(sequence:)— scripted responses in order; an extra request fails with a descriptiveSequenceExhaustederror.StubURLProtocol.session(recording:returning:)— record into aRequestRecorderand always reply the same way..failure(URLError(...))outcomes to simulate transport errors.RecordedRequest, a value wrapper around the request that was sent, withurlRequest,url,method,path,headers,queryItems,queryDictionary,body,bodyString,jsonBody(),jsonArrayBody()andformBody. The JSON accessors throw, so a malformed body fails the test instead of reading asnil. Wrap a request you built yourself withRecordedRequest(try request.urlRequest())to assert on it.
Every stub session has its own handler, so suites run safely in parallel.
Full API reference and articles (Getting Started, A Real-World Example, Recipes, Building Requests, Parsing Responses, Request Bodies) are published as Documentation.
The same documentation is bundled as a DocC catalog inside the package; in Xcode, choose Product ▸ Build Documentation to browse it locally.
- Swift 6.2 toolchain (Xcode 17+)
- iOS 13+, macOS 10.15+, tvOS 13+, watchOS 6+
MIT — see LICENSE.