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
APIClient
Extensions 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
:
modifyRequest
modifiers.configs
modifiers.
Executing an operation on the client involves:
withRequest
methods.
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:
.http
for HTTP requests usingtry await
syntax..httpPublisher
for HTTP requests with Combine syntax..httpDownload
for HTTP download requests usingtry await
syntax..mock
for mock requests usingtry await
syntax.
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:
.decodable
for decoding a response into a Decodable type..data
for obtaining a raw Data response..void
for ignoring the response..instance
for receiving a response of the same type asAPIClientCaller
returns. 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:
.bodyEncoder
for encoding a request body. Built-in encoders include.json
,.formURL
and.multipartFormData
..bodyDecoder
for decoding a request body. The built-in decoder is.json
..queryEncoder
for encoding a query. The built-in encoder is.query
..errorDecoder
for 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.Mockable
protocol allows any request returning aMockable
response to be mocked even without the.mock(_:)
modifier..usingMocksPolicy
configuration defines whether to use mocks, customizable with.usingMocks(policy:)
modifier. By default, mocks are ignored in thelive
environment 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.
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) // 3
swift-api-client
provides a set of macros for easier API declarations.
API
macro that generates an API client struct.Path
macro 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.7
import PackageDescription
let package = Package(
name: "SomeProject",
dependencies: [
.package(url: "https://github.com/dankinsoid/swift-api-client.git", from: "1.7.10")
],
targets: [
.target(
name: "SomeProject",
dependencies: [
.product(name: "SwiftAPIClient", package: "swift-api-client"),
]
)
]
)
$ swift build
Daniil Voidilov, [email protected]
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.