Skip to content

A lightweight, zero-dependency Swift networking library designed for type-safe HTTP requests using modern Swift concurrency

License

Notifications You must be signed in to change notification settings

otaviocc/MicroClient

Repository files navigation

MicroClient

codecov Check Runs Mastodon Follow

A lightweight, zero-dependency Swift networking library designed for type-safe HTTP requests using modern Swift concurrency.

Features

  • 🔒 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+

Requirements

  • Swift 6.0+
  • macOS 12.0+ / iOS 15.0+

Installation

Swift Package Manager

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")
]

Quick Start

1. Create a Configuration

import MicroClient

let configuration = NetworkConfiguration(
    session: .shared,
    baseURL: URL(string: "https://api.example.com")!
)

let client = NetworkClient(configuration: configuration)

2. Define Your Models

struct User: Codable {
    let id: Int
    let name: String
    let email: String
}

struct CreateUserRequest: Encodable {
    let name: String
    let email: String
}

3. Make Requests

// 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)

Architecture

MicroClient is built around four core components that work together:

NetworkClient

The main client interface providing an async/await API:

public protocol NetworkClientProtocol {
    func run<RequestModel, ResponseModel>(
        _ networkRequest: NetworkRequest<RequestModel, ResponseModel>
    ) async throws -> NetworkResponse<ResponseModel>
}

NetworkRequest

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
}

NetworkResponse

Wraps decoded response with original URLResponse metadata:

public struct NetworkResponse<ResponseModel> {
    public let value: ResponseModel
    public let response: URLResponse
}

NetworkConfiguration

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?
}

Advanced Usage

Automatic Retries

Configure automatic retries for failed requests.

Global Configuration

Set a default retry strategy for all requests in NetworkConfiguration:

let configuration = NetworkConfiguration(
    baseURL: URL(string: "https://api.example.com")!,
    retryStrategy: .retry(count: 3)
)

Per-Request Override

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
)

Advanced Logging

Enable detailed logging for requests and responses.

Default Logger

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
)

Custom Logger

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
)

Request Interceptors

Modify requests before they're sent using synchronous or asynchronous interceptors.

Synchronous Interceptor

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
    }
)

Asynchronous Interceptor

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.

Custom Encoders/Decoders

Override global configuration per request:

let customDecoder = JSONDecoder()
customDecoder.dateDecodingStrategy = .iso8601

let request = NetworkRequest<VoidRequest, TimestampedResponse>(
    path: "/events",
    method: .get,
    decoder: customDecoder
)

Form Data

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")
    ]
)

Query Parameters

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")
    ]
)

Error Handling

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
}

Testing

MicroClient is designed with testing in mind. The protocol-based architecture makes it easy to create mocks.

Development

Building

swift build

Testing

swift test

Linting

SwiftLint is integrated and run during build.

License

MicroClient is available under the MIT license. See the LICENSE file for more info.