futuredapp/ftapikit
Declarative async/await REST API framework using Swift Concurrency and Codable.
Requirements
- Swift 6.1+
- iOS 15+, macOS 12+, tvOS 15+, watchOS 8+
Installation
Add the following line to your Swift Package Manager dependencies:
.package(url: "https://github.com/futuredapp/FTAPIKit.git", from: "2.0.0")Features
The main feature of this library is to provide documentation-like API for defining web services. This is achieved using declarative and protocol-oriented programming in Swift.
The framework provides two core protocols reflecting the physical infrastructure:
URLServerprotocol defining single web service with built-in URLSession support.Endpointprotocol defining access points for resources.
Combining instances of type conforming to URLServer and Endpoint we can build request. URLServer has convenience methods for calling endpoints using URLSession.
[Architecture]
This package contains predefined Endpoint protocols. Use cases like multipart upload, automatic encoding/decoding are separated in various protocols for convenience.
Endpointprotocol has empty body. Typically used inGETendpoints.DataEndpointsends provided data in body.UploadEndpointuploads file from a URL usingURLSessionupload task.MultipartEndpointcombines body parts intoInputStreamand sends them to server.
Body parts are represented by MultipartBodyPart struct and provided to the endpoint in an array as data, a file URL or a stream factory.
URLEncodedEndpointsends body in URL query format.RequestEndpointhas encodable request which is encoded using encoding
of the URLServer instance.
[Endpoint types]
Usage
Defining web service (server)
Firstly we need to define our server. Structs are preferred but not required:
struct HTTPBinServer: URLServer {
let baseUri = URL(string: "http://httpbin.org/")!
let urlSession = URLSession(configuration: .default)
}If we want to use custom formatting we just need to add our encoding/decoding configuration:
struct HTTPBinServer: URLServer {
...
let decoding: Decoding = JSONDecoding { decoder in
decoder.keyDecodingStrategy = .convertFromSnakeCase
}
let encoding: Encoding = JSONEncoding { encoder in
encoder.keyEncodingStrategy = .convertToSnakeCase
}
}If we need to create specific request, add some headers, usually to provide authorization we can override default request building mechanism.
struct HTTPBinServer: URLServer {
...
func buildRequest(endpoint: Endpoint) async throws -> URLRequest {
var request = try buildStandardRequest(endpoint: endpoint)
request.addValue("MyApp/1.0.0", forHTTPHeaderField: "User-Agent")
return request
}
}Defining endpoints
Most basic GET endpoint can be implemented using Endpoint protocol, all default properties are inferred.
struct GetEndpoint: Endpoint {
let path = "get"
}Let's take more complicated example like updating some model. We need to supply encodable request and decodable response.
struct UpdateUserEndpoint: RequestResponseEndpoint {
typealias Response = User
let request: User
let path = "user"
}Endpoint requires Sendable, because requests are executed off the caller's actor. Structs of value types conform with no annotation; carry copies rather than references.
Executing the request
When we have server and endpoint defined we can call the web service using async/await:
let server = HTTPBinServer()
let endpoint = UpdateUserEndpoint(request: user)
let updatedUser = try await server.call(response: endpoint)Async buildRequest
One of the key features in FTAPIKit 2.0 is the ability to use async operations in buildRequest. This enables use cases like:
- Token Refresh: Await token refresh before building the request
- Dynamic Configuration: Fetch configuration or headers asynchronously
- Rate Limiting: Implement delays or throttling
Example with async token refresh:
struct MyServer: URLServer {
let baseUri = URL(string: "https://api.example.com")!
let tokenManager: TokenManager
func buildRequest(endpoint: Endpoint) async throws -> URLRequest {
// Refresh token if needed
await tokenManager.refreshIfNeeded()
var request = try buildStandardRequest(endpoint: endpoint)
request.addValue("Bearer \(tokenManager.token)", forHTTPHeaderField: "Authorization")
return request
}
}Request Configuration at Call Site
For scenarios where you need to configure requests at the call site (rather than in the server), use the RequestConfiguring protocol. This is useful for:
- Adding authorization headers in an API service layer
- Per-request configuration that varies by context
- Keeping server implementations simple and reusable
struct AuthorizedConfiguration: RequestConfiguring {
let authService: AuthService
func configure(_ request: inout URLRequest) async throws {
let token = try await authService.getValidAccessToken()
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
}
// Usage - configuration is optional with nil default
let server = HTTPBinServer()
let authConfig = AuthorizedConfiguration(authService: authService)
// Public endpoint - no configuration needed
let publicData = try await server.call(response: publicEndpoint)
// Protected endpoint - with configuration
let protectedData = try await server.call(response: protectedEndpoint, configuring: authConfig)Network Observers
Monitor request lifecycle with the NetworkObserver protocol:
final class LoggingObserver: NetworkObserver {
func willSendRequest(_ request: URLRequest) -> String {
let id = UUID().uuidString
print("[\(id)] Sending: \(request.url!)")
return id
}
func didReceiveResponse(for request: URLRequest, response: URLResponse?, data: Data?, context: String) {
print("[\(context)] Received response")
}
func didFail(request: URLRequest, error: Error, context: String) {
print("[\(context)] Failed: \(error)")
}
}
struct MyServer: URLServer {
let baseUri = URL(string: "https://api.example.com")!
let networkObservers: [any NetworkObserver] = [LoggingObserver()]
}Error Handling
The framework uses the APIError protocol for error handling. The default implementation APIError.Standard covers common cases:
do {
let response = try await server.call(response: endpoint)
} catch let error as APIError.Standard {
switch error {
case .connection(let urlError):
// Network connectivity issue
case .client(let statusCode, _, _):
// 4xx client error
case .server(let statusCode, _, _):
// 5xx server error
case .decoding(let decodingError):
// Response parsing failed
default:
break
}
}For custom error parsing, define a type conforming to APIError and set it as the ErrorType on your server:
struct MyServer: URLServer {
typealias ErrorType = MyCustomError
let baseUri = URL(string: "https://api.example.com")!
}Contributors
Current maintainer and main contributor is Matěj Kašpar Jirásek, <matej.jirasek@futured.app>.
We want to thank other contributors, namely:
License
FTAPIKit is available under the MIT license. See the LICENSE file for more information.
Package Metadata
Repository: futuredapp/ftapikit
Default branch: main
README: README.md