Skip to content

Repository files navigation

FTAPIKit logo

FTAPIKit

License

CI

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.

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:

  • URLServer protocol defining single web service with built-in URLSession support.
  • Endpoint protocol 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.

  • Endpoint protocol has empty body. Typically used in GET endpoints.
  • DataEndpoint sends provided data in body.
  • UploadEndpoint uploads file from a URL using URLSession upload task.
  • MultipartEndpoint combines body parts into InputStream and sends them to server. Body parts are represented by MultipartBodyPart struct and provided to the endpoint in an array.
  • URLEncodedEndpoint sends body in URL query format.
  • RequestEndpoint has 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:

structHTTPBinServer:URLServer{letbaseUri=URL(string:"http://httpbin.org/")!
leturlSession=URLSession(configuration:.default)}

If we want to use custom formatting we just need to add our encoding/decoding configuration:

structHTTPBinServer:URLServer{...letdecoding:Decoding=JSONDecoding{ decoder in
decoder.keyDecodingStrategy =.convertFromSnakeCase
}letencoding: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.

structHTTPBinServer:URLServer{...func buildRequest(endpoint:Endpoint)asyncthrows->URLRequest{varrequest=trybuildStandardRequest(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.

structGetEndpoint:Endpoint{letpath="get"}

Let's take more complicated example like updating some model. We need to supply encodable request and decodable response.

structUpdateUserEndpoint:RequestResponseEndpoint{typealiasResponse=Userletrequest:Userletpath="user"}

Executing the request

When we have server and endpoint defined we can call the web service using async/await:

letserver=HTTPBinServer()letendpoint=UpdateUserEndpoint(request: user)letupdatedUser=tryawait 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:

structMyServer:URLServer{letbaseUri=URL(string:"https://api.example.com")!
lettokenManager:TokenManagerfunc buildRequest(endpoint:Endpoint)asyncthrows->URLRequest{
// Refresh token if needed
await tokenManager.refreshIfNeeded()varrequest=trybuildStandardRequest(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
structAuthorizedConfiguration:RequestConfiguring{letauthService:AuthServicefunc configure(_ request:inoutURLRequest)asyncthrows{lettoken=tryawait authService.getValidAccessToken()
request.setValue("Bearer \(token)", forHTTPHeaderField:"Authorization")}}
// Usage - configuration is optional with nil default
letserver=HTTPBinServer()letauthConfig=AuthorizedConfiguration(authService: authService)
// Public endpoint - no configuration needed
letpublicData=tryawait server.call(response: publicEndpoint)
// Protected endpoint - with configuration
letprotectedData=tryawait server.call(response: protectedEndpoint, configuring: authConfig)

Network Observers

Monitor request lifecycle with the NetworkObserver protocol:

finalclassLoggingObserver:NetworkObserver{func willSendRequest(_ request:URLRequest)->String{letid=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)")}}structMyServer:URLServer{letbaseUri=URL(string:"https://api.example.com")!
letnetworkObservers:[anyNetworkObserver]=[LoggingObserver()]}

Error Handling

The framework uses the APIError protocol for error handling. The default implementation APIError.Standard covers common cases:

do{letresponse=tryawait server.call(response: endpoint)}catchlet 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:

structMyServer:URLServer{typealiasErrorType=MyCustomErrorletbaseUri=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.

About

Declarative and generic REST API framework using Codable.

Topics

Resources

Stars

22 stars

Watchers

6 watching

Forks

Releases

Used by

Contributors

Languages