Contents

artemkalinovsky/Kite

A Swift 6 networking library with async/await, JSON/XML deserialization, and auth header support — running on iOS/macOS/tvOS/watchOS/visionOS/Linux/Windows/Android!

Features

  • async/await-first request execution
  • Small protocol-based request model
  • Built-in JSON and XML response deserializers
  • Raw-data and no-op deserializers for simple endpoints
  • Query-string, JSON body, auth-header, and multipart upload support
  • Zero external dependencies
  • Explicit error behavior for auth failures, decode failures, and non-2xx responses

Requirements

  • Swift 6
  • Apple platforms: macOS 12+, iOS 15+, tvOS 15+, watchOS 8+, visionOS 1+
  • Linux: any Swift 6-supported distribution
  • Windows: any Swift 6-supported release
  • Android: experimental via the nightly Swift SDK for Android

Installation 📦

Swift Package Manager

In Xcode, choose:

File -> Add Package Dependencies... -> Up to Next Major Version starting at 5.0.0

Or add Kite to Package.swift:

.package(url: "https://github.com/artemkalinovsky/Kite.git", from: "5.0.0")

Example:

// swift-tools-version:6.0
import PackageDescription

let package = Package(
    name: "MyPackage",
    products: [
        .library(
            name: "MyPackage",
            targets: ["MyPackage"]
        )
    ],
    dependencies: [
        .package(url: "https://github.com/artemkalinovsky/Kite.git", from: "5.0.0")
    ],
    targets: [
        .target(
            name: "MyPackage",
            dependencies: ["Kite"]
        )
    ]
)

Quick Start 🧑‍💻

Suppose you want to fetch users from this JSON payload:

{
  "results": [
    {
      "name": {
        "first": "brad",
        "last": "gibson"
      },
      "email": "brad.gibson@example.com"
    }
  ]
}

Create the client:

import Kite

let apiClient = APIClient()

Define the response model:

struct User: Decodable {
    struct Name: Decodable {
        let first: String
        let last: String
    }

    let name: Name
    let email: String
}

Define the request:

import Foundation
import Kite

struct FetchRandomUsersRequest: DeserializeableRequestProtocol {
    var baseURL: URL { URL(string: "https://randomuser.me")! }
    var path: String { "api" }

    var deserializer: any ResponseDataDeserializer<[User]> {
        JSONDeserializer<User>.collectionDeserializer(keyPath: "results")
    }
}

Execute it:

let (users, _) = try await apiClient.execute(request: FetchRandomUsersRequest())

Request Defaults

HTTPRequestProtocol keeps the required surface deliberately small:

  • baseURL is the only required property.
  • path defaults to "".
  • method defaults to .get.
  • parameters defaults to nil.
  • headers defaults to [:].
  • multipartFormData defaults to nil.

Parameter encoding is determined by the request shape:

  • For .get requests, parameters are encoded as URL query items.
  • For non-GET requests without multipartFormData, parameters are encoded as a JSON body and Content-Type is set to application/json.
  • When multipartFormData is present, parameters are included as text form fields alongside the files in the same multipart body.

Deserializers

Kite ships with three built-in deserializer styles:

  • VoidDeserializer() for endpoints where you only care whether the request succeeded
  • RawDataDeserializer() when you want the raw response bytes
  • JSONDeserializer for Decodable models
  • XMLDeserializer for types that conform to XMLObjectDeserialization — built into Kite, no extra import needed

Examples:

let users = JSONDeserializer<User>.collectionDeserializer(keyPath: "results")
let profile = JSONDeserializer<User>.singleObjectDeserializer()
let feed = XMLDeserializer<FeedItem>.collectionDeserializer(keyPath: "response", "items", "item")

To use XMLDeserializer, conform your model to XMLObjectDeserialization:

import Kite

struct FeedItem: XMLObjectDeserialization {
    let title: String

    static func deserialize(_ node: XMLIndexer) throws -> Self {
        FeedItem(title: try node["title"].value())
    }
}

Authenticated Requests

Conform to AuthRequestProtocol when the endpoint requires an Authorization header:

import Foundation
import Kite

struct FetchProfileRequest: AuthRequestProtocol, DeserializeableRequestProtocol {
    let accessToken: String

    var baseURL: URL { URL(string: "https://api.example.com")! }
    var path: String { "profile" }

    var deserializer: any ResponseDataDeserializer<User> {
        JSONDeserializer<User>.singleObjectDeserializer()
    }
}

By default, Kite sends:

Authorization: Bearer <accessToken>

If your backend uses a different prefix, override accessTokenPrefix. If your backend expects a different authorization header name or casing, override authorizationHeaders.

Raw Data Requests

Use RawDataDeserializer when the endpoint does not return JSON or XML:

import Foundation
import Kite

struct DownloadAvatarRequest: DeserializeableRequestProtocol {
    var baseURL: URL { URL(string: "https://cdn.example.com")! }
    var path: String { "avatar.png" }

    var deserializer: any ResponseDataDeserializer<Data> {
        RawDataDeserializer()
    }
}

Multipart Uploads

Provide a [String: URL] dictionary through multipartFormData to upload files:

import Foundation
import Kite

struct UploadAvatarRequest: AuthRequestProtocol, DeserializeableRequestProtocol {
    let accessToken: String
    let imageURL: URL

    var baseURL: URL { URL(string: "https://api.example.com")! }
    var path: String { "upload" }
    var method: HTTPMethod { .post }
    var multipartFormData: [String: URL]? { ["file": imageURL] }

    var deserializer: any ResponseDataDeserializer<URL> {
        JSONDeserializer<URL>.singleObjectDeserializer(keyPath: "avatar_url")
    }
}

Kite builds the multipart body and sets the correct Content-Type boundary automatically.

Error Handling

Kite keeps failure modes explicit:

  • URLError(.userAuthenticationRequired) when an authenticated request resolves to an empty Authorization header
  • APIClientError.unacceptableStatusCode for non-2xx HTTP responses
  • JSONDeserializerError and XMLDeserializerError for decode failures

Example:

do {
    let (users, _) = try await apiClient.execute(request: FetchRandomUsersRequest())
    print(users)
} catch let error as APIClientError {
    print(error.localizedDescription)
} catch {
    print(error.localizedDescription)
}

Project Status

Kite is production-ready. Pull requests, questions, and suggestions are welcome.

Apps Using Kite

License 📄

Kite is released under the MIT license. See LICENSE for details.

Package Metadata

Repository: artemkalinovsky/Kite

Stars: 44

Forks: 3

Open issues: 0

Default branch: main

Primary language: swift

License: MIT

Topics: alamofire, android, api, api-rest, apimanager, jsonparser, linux, multiplatform, networklayer, networklibrary, swift, swift-framework, swift6, xmlparser

README: README.md