A lightweight, zero-dependency Swift networking library designed for type-safe HTTP requests using modern Swift concurrency.
- 🔒 Type-safe: Compile-time safety with generic request/response models
- ⚡ Modern: Built with Swift Concurrency (async/await)
- 🪶 Lightweight: Zero dependencies, minimal footprint
- ⚙️ Configurable: Global defaults with per-request customization
- 🔄 Interceptors: Middleware support for synchronous and asynchronous request modification
- 🔁 Automatic Retries: Built-in support for request retries
- 🪵 Advanced Logging: Customizable logging for requests and responses
- 📱 Cross-platform: Supports macOS 12+ and iOS 15+
- Swift 6.0+
- macOS 12.0+ / iOS 15.0+
Add MicroClient to your project using Xcode's package manager or by adding it to your Package.swift
:
dependencies: [
.package(url: "https://github.com/otaviocc/MicroClient", from: "0.0.17")
]
import MicroClient
let configuration = NetworkConfiguration(
session: .shared,
baseURL: URL(string: "https://api.example.com")!
)
let client = NetworkClient(configuration: configuration)
struct User: Codable {
let id: Int
let name: String
let email: String
}
struct CreateUserRequest: Encodable {
let name: String
let email: String
}
// GET request
let getUserRequest = NetworkRequest<VoidRequest, User>(
path: "/users/123",
method: .get
)
let userResponse = try await client.run(getUserRequest)
let user = userResponse.value
// POST request with body
let createUserRequest = NetworkRequest<CreateUserRequest, User>(
path: "/users",
method: .post,
body: CreateUserRequest(name: "John Doe", email: "[email protected]")
)
let newUserResponse = try await client.run(createUserRequest)
MicroClient is built around four core components that work together:
The main client interface providing an async/await API:
public protocol NetworkClientProtocol {
func run<RequestModel, ResponseModel>(
_ networkRequest: NetworkRequest<RequestModel, ResponseModel>
) async throws -> NetworkResponse<ResponseModel>
}
Type-safe request definitions with generic constraints:
public struct NetworkRequest<RequestModel, ResponseModel>
where RequestModel: Encodable, ResponseModel: Decodable {
public let path: String?
public let method: HTTPMethod
public let queryItems: [URLQueryItem]?
public let formItems: [URLFormItem]?
public let body: RequestModel?
public let additionalHeaders: [String: String]?
public let retryStrategy: RetryStrategy?
// ... configuration overrides
}
Wraps decoded response with original URLResponse metadata:
public struct NetworkResponse<ResponseModel> {
public let value: ResponseModel
public let response: URLResponse
}
Centralized configuration with override capability:
public struct NetworkConfiguration: Sendable {
public let session: URLSessionProtocol
public let defaultDecoder: JSONDecoder
public let defaultEncoder: JSONEncoder
public let baseURL: URL
public let retryStrategy: RetryStrategy
public let logger: Logger?
public let logLevel: LogLevel
public let interceptor: NetworkRequestsInterceptor?
public let asyncInterceptor: NetworkAsyncRequestInterceptor?
}
Configure automatic retries for failed requests.
Set a default retry strategy for all requests in NetworkConfiguration
:
let configuration = NetworkConfiguration(
baseURL: URL(string: "https://api.example.com")!,
retryStrategy: .retry(count: 3)
)
Override the global retry strategy for a specific request:
let request = NetworkRequest<VoidRequest, User>(
path: "/users/123",
method: .get,
retryStrategy: .none // This request will not be retried
)
Enable detailed logging for requests and responses.
Use the built-in ConsoleLogger
to print logs to the console:
let configuration = NetworkConfiguration(
baseURL: URL(string: "https://api.example.com")!,
logger: ConsoleLogger(),
logLevel: .debug // Log debug, info, warning, and error messages
)
Provide your own logger by conforming to the Logger
protocol:
struct MyCustomLogger: Logger {
func log(level: LogLevel, message: String) {
// Integrate with your preferred logging framework
print("[\(level)] - \(message)")
}
}
let configuration = NetworkConfiguration(
baseURL: URL(string: "https://api.example.com")!,
logger: MyCustomLogger(),
logLevel: .info
)
Modify requests before they're sent using synchronous or asynchronous interceptors.
A synchronous interceptor is useful for quick modifications, like adding a static header.
let configuration = NetworkConfiguration(
baseURL: URL(string: "https://api.example.com")!,
interceptor: { request in
var mutableRequest = request
mutableRequest.setValue("Bearer <STATIC_TOKEN>", forHTTPHeaderField: "Authorization")
return mutableRequest
}
)
An asynchronous interceptor is ideal for operations that require waiting, such as refreshing an authentication token.
let configuration = NetworkConfiguration(
baseURL: URL(string: "https://api.example.com")!,
asyncInterceptor: { request in
var mutableRequest = request
let token = await tokenProvider.refreshToken()
mutableRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
return mutableRequest
}
)
The synchronous interceptor runs first, followed by the asynchronous one.
Override global configuration per request:
let customDecoder = JSONDecoder()
customDecoder.dateDecodingStrategy = .iso8601
let request = NetworkRequest<VoidRequest, TimestampedResponse>(
path: "/events",
method: .get,
decoder: customDecoder
)
Send form-encoded data:
let request = NetworkRequest<VoidRequest, LoginResponse>(
path: "/auth/login",
method: .post,
formItems: [
URLFormItem(name: "username", value: "user"),
URLFormItem(name: "password", value: "pass")
]
)
Add query parameters to requests:
let request = NetworkRequest<VoidRequest, SearchResults>(
path: "/search",
method: .get,
queryItems: [
URLQueryItem(name: "q", value: "swift"),
URLQueryItem(name: "limit", value: "10")
]
)
MicroClient provides structured error handling:
do {
let response = try await client.run(request)
// Handle success
} catch let error as NetworkClientError {
switch error {
case .malformedURL:
// Handle invalid URL
case .unknown:
// Handle unknown errors
}
} catch {
// Handle other errors
}
MicroClient is designed with testing in mind. The protocol-based architecture makes it easy to create mocks.
swift build
swift test
SwiftLint is integrated and run during build.
MicroClient is available under the MIT license. See the LICENSE file for more info.