DEV Community

Daniil Voidilov
Daniil Voidilov

Posted on

Meet swift-api-client

Modular Swift library for API design.

GitHub page

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()
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

Or use the method .call(_:as:) when you need to configure execution or response serialization:

try client.call(.httpPublisher, as: .decodable(SomeType.self))
  .sink { ... }
Enter fullscreen mode Exit fullscreen mode

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] {}
}
Enter fullscreen mode Exit fullscreen mode

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)