Kite is named after the kite bird, known for its lightness, speed, and agile flight. This Swift Package aims to embody those qualities—offering a lightweight, fast, and flexible networking layer that runs on Apple platforms, Linux, Windows, and Android.
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
- 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
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"]
)
]
)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())HTTPRequestProtocol keeps the required surface deliberately small:
baseURLis the only required property.pathdefaults to"".methoddefaults to.get.parametersdefaults tonil.headersdefaults to[:].multipartFormDatadefaults tonil.
Parameter encoding is determined by the request shape:
- For
.getrequests,parametersare encoded as URL query items. - For non-GET requests without
multipartFormData,parametersare encoded as a JSON body andContent-Typeis set toapplication/json. - When
multipartFormDatais present,parametersare included as text form fields alongside the files in the same multipart body.
Kite ships with three built-in deserializer styles:
VoidDeserializer()for endpoints where you only care whether the request succeededRawDataDeserializer()when you want the raw response bytesJSONDeserializerforDecodablemodelsXMLDeserializerfor types that conform toXMLObjectDeserialization— 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())
}
}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.
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()
}
}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.
Kite keeps failure modes explicit:
URLError(.userAuthenticationRequired)when an authenticated request resolves to an emptyAuthorizationheaderAPIClientError.unacceptableStatusCodefor non-2xx HTTP responsesJSONDeserializerErrorandXMLDeserializerErrorfor 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)
}Kite is production-ready. Pull requests, questions, and suggestions are welcome.
Kite is released under the MIT license. See LICENSE for details.