Type-safe Swift HTTP networking
SmartNet provides flexible, type-safe HTTP networking that adapts to any Swift project's programming paradigm. Zero dependencies, thread-safe by design.
- Three programming paradigms: async/await, Combine, closures
- Type-safe generic endpoints:
Endpoint<T>with automatic decoding - Flexible path matching: exact, wildcard, glob, regex patterns
- Configurable retry policies: exponential backoff, linear, immediate
- Middleware system: request/response interception with pattern matching
- File operations: upload/download with progress tracking
- Zero external dependencies
- Thread-safe by design
Add SmartNet to your Package.swift:
dependencies: [
.package(url: "https://github.com/vetrek/SmartNet.git", from: "2.0.1")
]let client = ApiClient(config: NetworkConfiguration(
baseURL: URL(string: "https://api.example.com")!
))
struct User: Codable {
let id: Int
let name: String
}
let user: User = try await client.request(
with: Endpoint(path: "users/1")
)Basic:
let client = ApiClient(config: NetworkConfiguration(
baseURL: URL(string: "https://api.example.com")!
))With headers and timeout:
let config = NetworkConfiguration(
baseURL: URL(string: "https://api.example.com")!,
headers: ["Content-Type": "application/json"],
queryParameters: ["apiKey": "your-key"],
requestTimeout: 30
)
let client = ApiClient(config: config)do {
let user: User = try await client.request(with: Endpoint(path: "users/1"))
print(user.name)
} catch {
print(error)
}var subscriptions = Set<AnyCancellable>()
client.request(with: Endpoint<User>(path: "users/1"))
.sink(receiveCompletion: { completion in
if case .failure(let error) = completion {
print("Error: \(error)")
}
}, receiveValue: { user in
print(user.name)
})
.store(in: &subscriptions)client.request(with: Endpoint<User>(path: "users/1")) { response in
switch response.result {
case .success(let user):
print(user.name)
case .failure(let error):
print(error.localizedDescription)
}
}GET with query parameters:
let endpoint = Endpoint<User>(
path: "users",
queryParameters: QueryParameters(parameters: [
"name": "John",
"limit": 10
])
)POST with body:
struct CreateUserRequest: Codable {
let name: String
let email: String
}
let endpoint = Endpoint<User>(
path: "users",
method: .post,
body: HTTPBody(encodable: CreateUserRequest(
name: "John",
email: "john@example.com"
))
)By default, SmartNet does not retry failed requests. Enable retries by configuring a retry policy using the static factory methods.
Enable retries globally:
let config = NetworkConfiguration(
baseURL: URL(string: "https://api.example.com")!,
retryPolicy: .exponential() // 3 retries with exponential backoff
)Built-in policies:
| Factory Method | Delay Pattern | Default Retries |
|---|---|---|
.exponential() |
1s, 2s, 4s, 8s... (with jitter) | 3 |
.linear() |
1s, 2s, 3s, 4s... | 3 |
.immediate() |
No delay | 1 |
.none |
No retries (default) | 0 |
Customize retry behavior:
// Custom exponential backoff
let config = NetworkConfiguration(
baseURL: url,
retryPolicy: .exponential(
maxRetries: 5,
baseDelay: 2.0,
jitter: false,
conditions: [.timeout, .serverError]
)
)Per-endpoint retry configuration:
let endpoint = Endpoint<User>(path: "users/1")
.retryPolicy(.exponential(maxRetries: 5))RetryCondition options:
.timeout- request timed out.connectionLost- network connection lost.networkFailure- general network failure.serverError- HTTP 5xx responses.rateLimited- HTTP 429 (respectsRetry-Afterheader).dnsFailure- DNS lookup failed
Intercept requests and responses with path-matched middleware. Uses the PathMatcher system for flexible routing.
Global middleware (all requests):
client.addMiddleware(
ApiClient.Middleware(
pathMatcher: PathMatcher.contains("/"),
preRequestCallback: { request in
var req = request
req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
return req
},
postResponseCallback: { data, response, error in
return .next
}
)
)Path-specific middleware:
client.addMiddleware(
ApiClient.Middleware(
pathMatcher: PathMatcher.contains("users"),
preRequestCallback: { request in
print("User request: \(request.url?.path ?? "")")
return request
},
postResponseCallback: { _, _, _ in .next }
)
)PathMatcher patterns:
| Factory Method | Description | Example |
|---|---|---|
.contains("/") |
Global (all paths) | Matches everything |
.contains("users") |
Contains segment | /api/users/123 |
.exact("/users") |
Exact match | /users only |
.wildcard("/users/*") |
Single segment wildcard | /users/123 but not /users/123/posts |
.glob("/api/**") |
Multi-segment wildcard | /api, /api/v1/users |
.regex("^/users/\\d+$") |
Regular expression | /users/123 |
Upload with multipart form:
let uploadTask = try client.upload(
with: MultipartFormEndpoint(
path: "upload",
form: MultipartFormData {
TextField("name", value: "avatar")
FileField("file", data: imageData, fileName: "photo.jpg")
}
)
)
.progress { progress in
print("Upload: \(progress.fractionCompleted * 100)%")
}
.response { response in
print("Done: \(response.result)")
}Download with progress:
let downloadTask = client.download(url: URL(string: "https://example.com/file.zip")!)?
.downloadProgress { progress, _ in
print("Download: \(progress.fractionCompleted * 100)%")
}
.response { response in
print("Saved to: \(response.result)")
}All errors are returned as NetworkError:
do {
let user: User = try await client.request(with: endpoint)
} catch let error as NetworkError {
switch error {
case .error(let statusCode, let data):
print("HTTP \(statusCode)")
case .timeout:
print("Request timed out")
case .connectionLost:
print("Connection lost")
case .parsingFailed(let error):
print("Decoding failed: \(error)")
default:
print("Error: \(error)")
}
}SmartNet is available under the MIT license.