SwiftRest is a Swift 6 REST client focused on clear APIs and safe concurrency.
SwiftRestClientis anactor.- Public models are
Sendable. - You can decode payloads and inspect headers in the same call.
- You can choose throw-based APIs or result-style APIs.
- Swift 6.0+
- iOS 15+
- macOS 12+
Use Swift Package Manager with:
https://github.com/ricky-stone/SwiftRest.git
- Questions and ideas: GitHub Discussions
- Bugs and feature requests: GitHub Issues
- Contributing guide:
CONTRIBUTING.md - Security reports:
SECURITY.md
import SwiftRest
struct User: Decodable, Sendable {
let id: Int
let name: String
}
let client = try SwiftRestClient("https://api.example.com")
let user: User = try await client.get("users/1")
print(user.name)import SwiftUI
import SwiftRest
struct User: Decodable, Sendable {
let id: Int
let name: String
}
@MainActor
final class UserViewModel: ObservableObject {
@Published var name: String = ""
@Published var errorText: String?
private let client = try? SwiftRestClient("https://api.example.com")
func load() async {
guard let client else {
errorText = "Client setup failed"
return
}
do {
let user: User = try await client.get("users/1")
name = user.name
errorText = nil
} catch let error as SwiftRestClientError {
errorText = error.userMessage
} catch {
errorText = error.localizedDescription
}
}
}let response: SwiftRestResponse<User> = try await client.getResponse("users/1")
if let user = response.data {
print("Name:", user.name)
}
print("Status:", response.statusCode)
print("Request-Id:", response.headers["x-request-id"] ?? "missing")@MainActor
final class ProfileViewModel: ObservableObject {
@Published var name: String = ""
@Published var requestId: String = ""
private let client = try! SwiftRestClient("https://api.example.com")
func load() async {
do {
let response: SwiftRestResponse<User> = try await client.getResponse("users/1")
name = response.data?.name ?? ""
requestId = response.headers["x-request-id"] ?? ""
} catch {
name = ""
requestId = ""
}
}
}struct CreateUser: Encodable, Sendable {
let name: String
}
let payload = CreateUser(name: "Ricky")
let created: User = try await client.post("users", body: payload)
let updated: User = try await client.put("users/1", body: CreateUser(name: "Ricky Stone"))
let patched: User = try await client.patch("users/1", body: ["name": "Ricky S."])let _: NoContent = try await client.delete("users/1")
let raw = try await client.deleteRaw("users/1", allowHTTPError: true)
print(raw.statusCode, raw.isSuccess)let client = try SwiftRestClient(
"https://api.example.com",
config: .standard.accessToken("YOUR_ACCESS_TOKEN")
)Runtime update:
await client.setAccessToken("NEW_TOKEN")
await client.clearAccessToken()let adminUser: User = try await client.get("users/1", authToken: "ONE_OFF_TOKEN")await client.setAccessTokenProvider {
// Return latest token (refresh from keychain/service if needed)
return "LATEST_TOKEN"
}Auth precedence:
- per-request token (
authToken:/request.authToken(...)) - provider token (
setAccessTokenProvider/config.accessTokenProvider(...)) - global token (
setAccessToken/config.accessToken(...))
When enabled, SwiftRest will:
- receive
401 - call your refresh callback
- retry the same request once with the refreshed token
let refresh = SwiftRestAuthRefresh {
// Call refresh endpoint and return new access token
return "fresh-token"
}
let client = try SwiftRestClient(
"https://api.example.com",
config: .standard
.accessToken("expired-token")
.authRefresh(refresh)
)
let profile: User = try await client.get("users/me")By default, refresh does not run for explicit per-request tokens.
Enable it if needed:
let refresh = SwiftRestAuthRefresh {
return "fresh-token"
}.appliesToPerRequestToken(true)import SwiftUI
import SwiftRest
@MainActor
final class SessionStore: ObservableObject {
@Published private(set) var accessToken: String = "expired-token"
func updateToken(_ token: String) {
accessToken = token
}
}
func makeClient(session: SessionStore) throws -> SwiftRestClient {
let refresh = SwiftRestAuthRefresh {
// Example only. Replace with your refresh API call.
let newToken = "fresh-token"
await session.updateToken(newToken)
return newToken
}
return try SwiftRestClient(
"https://api.example.com",
config: .standard
.accessToken(session.accessToken)
.authRefresh(refresh)
)
}struct UserQuery: Encodable, Sendable {
let page: Int
let search: String
let includeInactive: Bool
}
let query = UserQuery(page: 1, search: "ricky", includeInactive: false)
let users: [User] = try await client.get("users", query: query)@MainActor
final class SearchViewModel: ObservableObject {
@Published var users: [User] = []
private let client = try! SwiftRestClient("https://api.example.com")
func search(term: String) async {
struct UserQuery: Encodable, Sendable {
let page: Int
let search: String
let includeInactive: Bool
}
do {
let query = UserQuery(page: 1, search: term, includeInactive: false)
users = try await client.get("users", query: query)
} catch {
users = []
}
}
}Notes:
- Query models follow your client JSON key encoding strategy.
- Example:
.webAPIconvertsincludeInactivetoinclude_inactive.
- Example:
- You can also encode query models manually with
SwiftRestQuery.encode(...).
Use result APIs when you want explicit branches for success, API errors, and transport failures.
struct APIErrorModel: Decodable, Sendable {
let message: String
let code: String?
}
let result: SwiftRestResult<User, APIErrorModel> =
await client.getResult("users/1")
switch result {
case .success(let response):
print(response.data?.name ?? "none")
case .apiError(let decoded, let response):
print("Status:", response.statusCode)
print(decoded?.message ?? "No typed API error body")
case .failure(let error):
print(error.userMessage)
}@MainActor
final class ResultViewModel: ObservableObject {
enum State {
case idle
case loading
case loaded(String)
case apiError(String)
case transportError(String)
}
@Published var state: State = .idle
private let client = try! SwiftRestClient("https://api.example.com")
struct APIErrorModel: Decodable, Sendable {
let message: String
}
func load() async {
state = .loading
let result: SwiftRestResult<User, APIErrorModel> =
await client.getResult("users/1")
switch result {
case .success(let response):
state = .loaded(response.data?.name ?? "No data")
case .apiError(let decoded, _):
state = .apiError(decoded?.message ?? "Request failed")
case .failure(let error):
state = .transportError(error.userMessage)
}
}
}Available result APIs:
executeResult(...)getResult(...)deleteResult(...)postResult(...)putResult(...)patchResult(...)
let client = try SwiftRestClient(
"https://api.example.com",
config: .standard.debugLogging(true)
)let client = try SwiftRestClient(
"https://api.example.com",
config: .standard.debugLogging(.headers)
)let logging = SwiftRestDebugLogging(
isEnabled: true,
includeHeaders: true,
handler: { line in
print("NETWORK:", line)
}
)
let client = try SwiftRestClient(
"https://api.example.com",
config: .standard.debugLogging(logging)
)Sensitive headers are redacted by default (for example: Authorization, cookies, token/secret-like headers).
let client = try SwiftRestClient("https://api.example.com")let client = try SwiftRestClient(
"https://api.example.com",
config: .standard.dateDecodingStrategy(.iso8601)
)let client = try SwiftRestClient("https://api.example.com", config: .webAPI)var request = SwiftRestRequest(path: "legacy-endpoint", method: .get)
request.configureDateDecodingStrategy(.formatted(format: "yyyy-MM-dd HH:mm:ss"))
let legacy: User = try await client.execute(request, as: User.self)var request = SwiftRestRequest(path: "users", method: .get)
request.addHeader("X-App", "Demo")
request.addParameter("page", "1")
request.configureRetries(maxRetries: 2, retryDelay: 0.5)let request = try SwiftRestRequest.get("users")
.header("X-App", "Demo")
.query(UserQuery(page: 1, search: "ricky", includeInactive: false))do {
let user: User = try await client.get("users/does-not-exist")
print(user)
} catch let error as SwiftRestClientError {
print(error.userMessage)
if case .httpError(let details) = error {
print(details.statusCode)
print(details.headers["content-type"] ?? "n/a")
print(details.rawPayload ?? "")
}
} catch {
print(error.localizedDescription)
}When no config is passed, SwiftRest uses SwiftRestConfig.standard:
- Base header:
Accept: application/json - Timeout:
30seconds - Retry policy:
RetryPolicy.standard- Max attempts:
3 - Base delay:
0.5seconds - Retryable status codes:
408, 429, 500, 502, 503, 504
- Max attempts:
- JSON coding:
SwiftRestJSONCoding.foundationDefault - Global token: none
- Debug logging: disabled
- Auth refresh: disabled
SwiftRest is licensed under the MIT License. See LICENSE.txt.
Industry standard for MIT:
- You can use this in commercial/private/open-source projects.
- Keep the copyright + license notice when redistributing.
- Attribution is appreciated but not required by MIT.
Created and maintained by Ricky Stone.
Thanks to everyone who tests, reports issues, and contributes improvements.
Current source version marker: SwiftRestVersion.current == "3.4.0"