Modular Swift library for API design.
Key Features
- Minimal, intuitive syntax for API requests
- Built-in configuration sharing across requests
- First-class support for modern Swift features (async/await, Combine)
- Comprehensive tools for auth, mocking, and testing
- Highly extensible architecture
Why Another Networking Library?
Creating an API client for iOS projects has always been a non-trivial task. Native URLSession
is too low-level of a tool. Popular solutions like Alamofire
and Moya
help significantly, but they lack a built-in, universal approach for sharing configurations and logic across multiple requests, such as common headers or paths, authorization headers, token refreshing, mocks for debug or preview mode, testing, throttling, retrying, etc. Additionally, I found the method they provide for defining and calling API requests not convenient enough.
Therefore, I set a goal for myself to develop a library that offers the most minimalistic way possible to define API requests, including built-in simple tools for all common tasks. Additionally, the library should be easily extendable.
I want to briefly present the results of my work here.
Quick Start
First, let's start with a short code example:
// Create a root APIClient instance
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)
}
// Scope a `APIClient` 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()
Core Concepts
The core of the library is the APIClient
struct. To define API requests, you need to create an APIClient
instance with a base URL
. Then, you call modifiers such as .bodyEncoder(_:)
, .tokenRefresher(_:)
, .path(_:)
, etc. Each modifier returns a new APIClient, allowing you to branch the APIClient and share any configurations across APIClient
scopes. For instance, if you have two requests with a common Content-Language header but different paths, you would use the .header(.contentLanguage, "en")
modifier to create a common APIClient
instance for both requests. Then, create a client for each request from the common one using the .path(_:)
modifier.
let commonClient = client.header(.contentLanguage, "en")
let firstRequestClient = commonClient.path("first")
let secondRequestClient = commonClient.path("second")
Both firstRequestClient
and secondRequestClient
will build a request with the "Content-Language=en". There's no limitation on the configurations you can inherit. This could be request modifications like .query(_:)
, .path(_:)
, or execution middlewares like .throttle()
, .retry()
, or objects like .bodyEncoder()
, .bodyDecoder()
.
To execute an HTTP request, you can use a client as a function with ():
let response: SomeType = try await client()
Or use the method .call(_:as:)
when you need to configure execution or response serialization:
try client.call(.httpPublisher, as: .decodable(SomeType.self))
.sink { ... }
APIClient
is not limited to URLSession
HTTP requests. There's the possibility to set up a custom request caller like WebSocket (not built-in), or just a custom HTTP client instead of URLSession
, such as async-http-client
, for example.
What is APIClient
APIClient
is a struct that encapsulates a closure for creating a request and a typed dictionary of configurations, APIClient.Configs
. There are two primary ways to extend an APIClient
:
- Through
.modifyRequest
modifiers. - Through
.configs
modifiers.
Executing an operation with the client involves the use of .withRequest
methods. All built-in extensions utilize these modifiers to provide flexibility and power in configuring and executing network requests.
The library includes a wide array of built-in configurations, such as support for request body and query Encodable
, Multipart form data encoding, response body Decodable
support, requests retrying and throttling, token refreshing, response mocking, logging, etc.
Macros
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] {}
}
Next Steps
This article is introductory, so I will not delve into the entire functionality here. If you're interested, I welcome you to visit the GitHub page, where you'll find a more detailed README and a full example. Also you can check the autogenerated docs page. Additionally, on GitHub, there is a discussions section where I'm eager to hear any comments and suggestions.
Top comments (0)