Resting is an async/await-first Swift package for common REST client work.
It provides typed request definitions, validated HTTP responses, a shared typed
error model, secondary Combine compatibility APIs, and per-operation download
handles with isolated progress and cancellation.
- Swift 6.3
- Apple platforms supported by
Package.swift: iOS 15+, macOS 12+, watchOS 8+, tvOS 15+, visionOS 1+
Add Resting to your Swift Package Manager dependencies:
dependencies: [
.package(url: "https://github.com/rocxteady/Resting.git", branch: "main")
]Import the package and configure a client:
import Foundation
import Resting
let configuration = RestClientConfiguration(
sessionConfiguration: .default,
decoder: JSONDecoder(),
encoder: JSONEncoder(),
defaultHeaders: ["Accept": "application/json"]
)
let client = RestClient(configuration: configuration)Build a request with the request style that matches the operation:
let request = RequestDefinition.json(
url: URL(string: "https://api.example.com/articles")!,
method: .post,
body: CreateArticleRequest(title: "Modern API"),
headers: ["Authorization": "Bearer token"]
)Execute raw or decoded responses:
let rawPayload = try await client.execute(request)
let rawData = rawPayload.value
let article: Article = try await client.execute(
.query(url: URL(string: "https://api.example.com/articles/1")!),
as: Article.self
)Use the specialized constructors instead of a single overloaded request type:
let search = RequestDefinition.query(
url: URL(string: "https://api.example.com/articles")!,
queryItems: [URLQueryItem(name: "page", value: "1")]
)
let form = RequestDefinition.form(
url: URL(string: "https://api.example.com/login")!,
fields: ["email": "hello@example.com", "password": "secret"]
)
let json = RequestDefinition.json(
url: URL(string: "https://api.example.com/articles")!,
body: CreateArticleRequest(title: "Modern API")
)
let raw = RequestDefinition.raw(
url: URL(string: "https://api.example.com/upload")!,
body: Data("payload".utf8),
contentType: "application/octet-stream"
)Combine remains available as a secondary surface when an app still needs publisher-based integration:
Returned publishers retain their client while the publisher or an active subscription is retained, so a stored publisher remains safe to subscribe to after other client references are released.
import Combine
let cancellable = client
.publisher(for: request, as: Article.self)
.sink(
receiveCompletion: { completion in
if case .failure(let error) = completion {
print(error.localizedDescription)
}
},
receiveValue: { article in
print(article)
}
)Each download returns its own TransferHandle, so overlapping transfers do not
share hidden mutable client state:
let handle = client.download(
.download(url: URL(string: "https://example.com/archive.zip")!)
)
handle.observeProgress { progress in
print(progress.fractionCompleted)
}
let fileURL = try await handle.valueCancellation is per handle:
handle.cancel()For source compatibility, RestClient retains its public
URLSessionDownloadDelegate conformance and callbacks while using a private
delegate for its own session lifecycle.
All public execution paths use the same RestingError model:
Async, Combine, and downloads accept only final HTTP status codes in
200..<300. Missing and non-HTTP responses fail with invalidResponse;
other statuses retain their code and any available non-empty response bytes.
Rejected downloads never return a file URL and their temporary files are
removed best-effort. Async and Combine decoding failures both retain the
original response bytes, including empty data.
invalidRequest(reason:)transport(URLError)invalidResponsestatusCode(Int, Data?)decoding(underlying:data:)cancelledfileSystem(underlying:)
Example:
do {
let article: Article = try await client.execute(request, as: Article.self)
print(article)
} catch let error as RestingError {
switch error {
case .statusCode(let code, _):
print("Unexpected status:", code)
case .cancelled:
print("Cancelled")
default:
print(error.localizedDescription)
}
}This release intentionally introduces source-breaking cleanup to remove the old flat API.
- Replace
RequestConfigurationwithRequestDefinition. - Replace
fetch(with:)withexecute(_:),executeData(_:), orexecute(_:as:). - Replace ad hoc request payload overloads with
.query,.form,.json,.jsonData,.raw, and.download. - Replace client-global
download(with:completion:progress:)pluscancel()with per-operationTransferHandleinstances. - Replace legacy
RestingError.urlMalformed,wrongParameterType, andunknownhandling with the richer typed error cases listed above. - Remove references to
ResponseValidator. Validation is now an internal, fixed200..<300rule with no replacement customization API.
RestClient is safe to reuse for overlapping operations. Each handle owns its
own progress and cancellation, and releasing the client requires no explicit
shutdown call; its session and delegate are invalidated and released
automatically after active work finishes.
Project standards for contributors are tracked in .specify/memory/constitution.md.
Public API changes are expected to include tests, docs, and localized
user-facing strings where applicable.
This package is available under the MIT license. See LICENSE.