Declarative async/await REST API framework using Swift Concurrency and Codable. With standard implementation using URLSession and JSON encoder/decoder. Built for Swift 6.1+ with full concurrency safety.
- Swift 6.1+
- iOS 15+, macOS 12+, tvOS 15+, watchOS 8+
Add the following line to your Swift Package Manager dependencies:
.package(url: "https://github.com/futuredapp/FTAPIKit.git", from: "2.0.0")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.
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 byMultipartBodyPartstruct and provided to the endpoint in an array.URLEncodedEndpointsends body in URL query format.RequestEndpointhas encodable request which is encoded using encoding of theURLServerinstance.
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
}
}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"
}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)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
}
}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)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()]
}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")!
}Current maintainer and main contributor is Matěj Kašpar Jirásek, matej.jirasek@futured.app.
We want to thank other contributors, namely:
FTAPIKit is available under the MIT license. See the LICENSE file for more information.
