swift-api-client is a comprehensive and modular Swift library for API design.
- Table of Contents
- Main Goals of the Library
- Usage
- What is
APIClient - Built-in
APIClientExtensions APIClient.Configs- Macros
- URL and URLComponents Extensions
- Introducing
swift-api-client-addons - Installation
- Author
- License
- Contributing
- Minimalistic and intuitive syntax.
- Reusability, allowing for the injection of configurations across all requests.
- Extensibility and modularity.
- A simple core offering a wide range of possibilities.
- Facilitation of testing, mocking and debugging (logs, metrics).
The core of the library is the APIClient struct, serving both as a request builder and executor. It is a generic struct, enabling use for any task associated with URL request.
The branching and configuration injection/overriding capabilities of APIClient, extending to all its child instances, facilitate the effortless recycling of networking logic and tasks, thereby eliminating the need for copy-pasting.
While a full example is available in the Example folder, here is a simple usage example:
let client = APIClient(url: baseURL)
.bodyDecoder(.json(dateDecodingStrategy: .iso8601))
.bodyEncoder(.json(dateEncodingStrategy: .iso8601))
.errorDecoder(.decodable(APIError.self))
.tokenRefresher { refreshToken, client, _ in
guard let refreshToken else { throw APIError.noRefreshToken }
let tokens: AuthTokens = try await client("auth", "token")
.body(["refresh_token": refreshToken])
.post()
return (tokens.accessToken, tokens.refreshToken, tokens.expiresIn)
} auth: {
.bearer(token: $0)
}
// Create a `APIClient` instance for the /users path
let usersClient = client("users")
// GET /users?name=John&limit=1
let john: User = try await usersClient
.query(["name": "John", "limit": 1])
.auth(enabled: false)
.get()
// Create a `APIClient` instance for /users/{userID} path
let johnClient = usersClient(john.id)
// GET /user/{userID}
let user: User = try await johnClient.get()
// PUT /user/{userID}
try await johnClient.body(updatedUser).put()
// DELETE /user/{userID}
try await johnClient.delete()Also, you can use macros for API declaration:
/// /pet
@Path
struct Pet {
/// PUT /pet
@PUT("/") public func update(_ body: PetModel) -> PetModel {}
/// POST /pet
@POST("/") public func add(_ body: PetModel) -> PetModel {}
/// GET /pet/findByStatus
@GET public func findByStatus(@Query _ status: PetStatus) -> [PetModel] {}
/// GET /pet/findByTags
@GET public func findByTags(@Query _ tags: [String]) -> [PetModel] {}
}APIClient is a struct combining a closure for creating a URL request and a typed dictionary of configurations APIClient.Configs. There are two primary ways to extend a APIClient:
modifyRequestmodifiers.configsmodifiers.
Executing an operation on the client involves:
withRequestmethods.
All built-in extensions utilize these modifiers.
The full list is available in docs.
Numerous methods exist for modifying a URL request such as query, body, header, headers, method, path, body and more.
let client = APIClient(url: baseURL)
.method(.post)
.body(someEncodableBody)
.query(someEncodableQuery)
.header(.acceptEncoding, "UTF-8")The full list of modifiers is available in RequestModifiers.swift, all based on the modifyRequest modifier.
Notable non-obvious modifiers include:
.callAsFunction(path...)- as a shorthand for the.path(path...)modifier, allowingclient("path")instead ofclient.path("path").- HTTP method shorthands like
.get,.post,.put,.delete,.patch.
The methodcall(_ caller: APIClientCaller<...>, as serializer: Serializer<...>) is provided.
Examples:
try await client.call(.http, as: .decodable)
try await client.call(.http, as: .void)
try client.call(.httpPublisher, as: .decodable)There are also shorthands for built-in callers and serializers:
call()is equivalent tocall(.http, as: .decodable)orcall(.http, as: .void)callAsFunction()acts ascall(), simplifyingclient.delete()toclient.delete.call()orclient()instead ofclient.call(), etc.
Defines request execution with several built-in callers for various request types, including:
.httpfor HTTP requests usingtry awaitsyntax..httpPublisherfor HTTP requests with Combine syntax..httpDownloadfor HTTP download requests usingtry awaitsyntax..mockfor mock requests usingtry awaitsyntax.
All built-in HTTP callers use the .httpClient configuration, which can be customized with the .httpClient() modifier. The default .httpClient is URLSession. It's possible to customize the current .httpClient instance, for example, to use a custom URLSession configuration or async-http-client.
Custom callers can be created for different types of requests, such as WebSocket, GraphQL, etc.
Serializer is a struct that describes response serialization with several built-in serializers:
.decodablefor decoding a response into a Decodable type..datafor obtaining a raw Data response..voidfor ignoring the response..instancefor receiving a response of the same type asAPIClientCallerreturns. For HTTP requests, it isData.
The .decodable serializer uses the .bodyDecoder configuration, which can be customized with the .bodyDecoder modifier. The default bodyDecoder is JSONDecoder().
.retry(limit:)for retrying a request a specified number of times..throttle(interval:)for throttling requests with a specified interval..timeout(_:)for setting an execution timeout..waitForConnection()for waiting for a connection before executing a request..backgroundTask()for executing a request in the background task..retryIfFailedInBackground()for retrying a request if it fails in the background.
There are several built-in configurations for encoding and decoding:
.bodyEncoderfor encoding a request body. Built-in encoders include.json,.formURLand.multipartFormData..bodyDecoderfor decoding a request body. The built-in decoder is.json..queryEncoderfor encoding a query. The built-in encoder is.query..errorDecoderfor decoding an error response. The built-in decoder is.decodable(type).
These encoders and decoders can be customized with corresponding modifiers.
ContentSerializer is a struct that describes request body serialization, with one built-in content serializer: .encodable that utilizes the .bodyEncoder configuration.
Custom content serializers can be specified by passing a ContentSerializer instance to the .body(_:as:) modifier.
.auth and .isAuthEnabled configurations can be customized with .auth(_:) and .auth(enabled:) modifiers,
allowing the injection of an authentication type for all requests and enabling/disabling it for specific requests.
The .auth configuration is an AuthModifier instance with several built-in AuthModifier types:
.bearer(token:)for Bearer token authentication..basic(username:password:)for Basic authentication..apiKey(key:field:)for API Key authentication.
The .tokenRefresher(...) modifier can be used to specify a token refresher closure, which is called when a request returns a 401 status code. The refresher closure receives the cached refresh token, the client, and the response, and returns a new token, which is then used for the request. .refreshToken also sets the .auth configuration.
Built-in tools for mocking requests include:
.mock(_:)modifier to specify a mocked response for a request.Mockableprotocol allows any request returning aMockableresponse to be mocked even without the.mock(_:)modifier..usingMocksPolicyconfiguration defines whether to use mocks, customizable with.usingMocks(policy:)modifier. By default, mocks are ignored in theliveenvironment and used as specified for tests and SwiftUI previews.
Additionally, .mock(_:) as a APIClientCaller offers an alternative way to mock requests, like client.call(.mock(data), as: .decodable).
Custom HTTPClient instances can also be created and injected for testing or previews.
swift-api-client employs swift-log for logging, with .logger and .logLevel configurations customizable via logger and .log(level:) modifiers.
The default log level is .info. A built-in .none Logger is available to disable all logs.
Log example:
[29CDD5AE-1A5D-4135-B76E-52A8973985E4] ModuleName/FileName.swift/72
--> 🌐 PUT /petstore (9-byte body)
Content-Type: application/json
--> END PUT
[29CDD5AE-1A5D-4135-B76E-52A8973985E4]
<-- ✅ 200 OK (100ms, 15-byte body)
Log message format can be customized with the .loggingComponents(_:) modifier. For example, cURL logs can be enabled with .loggingComponents(.cURL).
swift-api-client employs swift-metrics for metrics, with .reportMetrics configuration customizable via .reportMetrics(_:) modifier.
swift-api-client reports:
api_client_requests_total: total requests count.api_client_responses_total: total responses count.api_client_errors_total: total errors count.http_client_request_duration_seconds: http requests duration.
A collection of config values is propagated through the modifier chain. These configs are accessible in all core methods: modifyRequest, withRequest, and withConfigs.
To create custom config values, extend the APIClient.Configs structure with a new property.
Use subscript with your property key path to get and set the value, and provide a dedicated modifier for clients to use when setting this value:
extension APIClient.Configs {
var myCustomValue: MyConfig {
get {
self[\.myCustomValue] ?? myDefaultConfig
}
set {
self[\.myCustomValue] = newValue
}
}
}
extension APIClient {
func myCustomValue(_ myCustomValue: MyConfig) -> APIClient {
configs(\.myCustomValue, myCustomValue)
}
}There is valueFor global method that allows you to define default values depending on the environment: live, test or preview.
All configs are collected in the final withRequest method and then passed to all modifiers, so the last defined value is used.
Note that all execution methods, like call, are based on the withRequest method.
For instance, the following code will print 3 in all cases:
let configs = try client
.configs(\.intValue, 1)
.modifyRequest { _, configs in
print(configs.intValue) // 3
}
.configs(\.intValue, 2)
.modifyRequest { _, configs in
print(configs.intValue) // 3
}
.configs(\.intValue, 3)
.withRequest { _, configs in
print(configs.intValue) // 3
return configs
}
print(configs.intValue) // 3swift-api-client provides a set of macros for easier API declarations.
APImacro that generates an API client struct.Pathmacro that generates an API client scope for the path.Cal(_ method:),GET,POST,PUT, etc macros for declaring API methods. Example:
/// /pet
@Path
struct Pet {
/// PUT /pet
@PUT("/") public func update(_ body: PetModel) -> PetModel {}
/// POST /pet
@POST("/") public func add(_ body: PetModel) -> PetModel {}
/// GET /pet/findByStatus
@GET public func findByStatus(@Query _ status: PetStatus) -> [PetModel] {}
/// GET /pet/findByTags
@GET public func findByTags(@Query _ tags: [String]) -> [PetModel] {}
/// /pet/{id}
@Path("{id}")
public struct PetByID {
/// GET /pet/{id}
@GET("/")
func get() -> PetModel {}
/// DELETE /pet/{id}
@DELETE("/")
func delete() {}
/// POST /pet/{id}
@POST("/") public func update(@Query name: String?, @Query status: PetStatus?) -> PetModel {}
/// POST /pet/{id}/uploadImage
@POST public func uploadImage(_ body: Data, @Query additionalMetadata: String? = nil) {}
}
}Macros are not necessary for using swift-api-client; they are just syntax sugar.
Sure, here is a concise section for your README that demonstrates the most convenient methods provided by your extensions:
These extensions provide convenient methods for configuring URLs and URLComponents, offering a fluent interface for setting path components, query parameters, and other URL components.
let url = URL(string: "https://example.com")!
.path("path1", "path2")
.query("key1", 1)These extensions simplify and streamline your URL building and modification processes in Swift.
To enhance your experience with swift-api-client, I'm excited to introduce swift-api-client-addons
— a complementary library designed to extend the core functionality of swift-api-client with additional features and utilities.
Create a Package.swift file.
// swift-tools-version:5.9
import PackageDescription
let package = Package(
name: "SomeProject",
dependencies: [
.package(url: "https://github.com/dankinsoid/swift-api-client.git", from: "1.44.0")
],
targets: [
.target(
name: "SomeProject",
dependencies: [
.product(name: "SwiftAPIClient", package: "swift-api-client"),
]
)
]
)$ swift buildDaniil Voidilov, voidilov@gmail.com
swift-api-client is available under the MIT license. See the LICENSE file for more info.
We welcome contributions to Swift-Networking! Please read our contributing guidelines to get started.