Generate RESTful APIs with Swift in 2023
In this blog post, I describe my experience using Swift OpenAPI Generator to generate a fully functional client API.
Previously I wrote an article on how to leverage OpenAPI and other open-source tools to generate a client API in Swift and how to test it with a mocked backend.
I outlined four significant steps.
Get a RESTful API definition of the backend
Mock the backend
Generate a Swift client library
Use the Swift client library in an application
In this article, I'll focus on steps three and four, for which you can now use the recently announced open-source project Swift OpenAPI Generator from Apple.
I recommend reading my previous article to learn more about steps one and two.
What is Swift OpenAPI Generator
Swift OpenAPI Generator is a SwiftPM plugin that takes an OpenAPI document and generates either the client code to perform HTTP calls or the server code to handle those calls.
The generated code translates between a type‑safe representation of each operation’s input and output, and the underlying HTTP request and response.
Swift OpenAPI Generator is a young project and, as of mid-June 2023, did not reach milestone version 1.0.
Nevertheless, the project must be taken seriously as similar open-open source projects may stop their development activities in favor of Apple's activities. The best example is SwagGen which did stop development.
Getting started with Swift OpenAPI Generator is easy thanks to their extensive DocC documentation, especially their tutorials like Generating a client in an Xcode project.
My goal for this blog post was to re-implement my previous example to generate a Swift client library for this particular specification of an IP geolocation API.
Generate a Swift client library
For generating a client API, I had to add the following package dependencies to my iOS application.
The package plugin (swift-openapi-generator), which performs code generation at build time.
You don't select any package product!
The runtime library (swift-openapi-runtime), which contains protocol definitions used by the generated code and extension libraries
A transport implementation, here swift-openapi-urlsession, which allows plugging in the chosen HTTP client library or server framework
Then I enabled the package plugin on my target
and added target dependencies for the runtime and transport libraries.
Finally, I added two files to my application target.
openapi.yaml
, the OpenAPI document describing your API.openapi-generator-config.yaml
, a configuration file for the plugin, which controls whether to generate client or server code.
Initially, I used Swift OpenAPI Generator 0.1.2 in which your files must use the y.aml
file extension. The alternative file extension .yml
did not work. I appreciate that support for .yml
extension was added in 0.1.3.
Once I built my project, the client API sources were generated in the derived folder, i.e. SourcePackages/plugins/<YourAppName>.output/<YourAppTargetName>/OpenAPIGenerator/GeneratedSources
I created a run script to view the generated sources in the Finder.
Using the build plugin and leaving the generated files in the derived folder has the advantage that the generated sources will always be in sync with the respective OpenAPI specification.
This is useful when the specification gets changed quite a lot.
However, I would generally prefer if the generated sources were under source control. I might ditch the build plugin, use the Swift OpenAPI generator as a command-line tool, and then check in the generated source files.
Use the Swift client library in an application
The generated sources contain a public struct Client: APIProtocol
with async functions for the HTTP operations.
Also a public enum Components
as a namespace with all the components of the OpenAPI document get generated.
Here is an example of how to use it.
import OpenAPIRuntime
import OpenAPIURLSession
let client = Client(
serverURL: URL(string: "http://localhost:3000")!,
transport: URLSessionTransport()
)
var model: Components.Schemas.inline_response_200?
let response = try await client.get_v1_(.init(query: .init(api_key: "fakeApiKey", ip_address: "73.158.231.173")))
switch response {
case .ok(let okresponse):
switch okresponse.body {
case .json(let result):
print("Hi, your IP address is \(result.ip_address ?? "") and you are located in \(result.city ?? ""), a city in the wonderful country \(result.country ?? "")")
}
case .undocumented(statusCode: let statusCode, let undocumentedPayload):
print("Undocumented response \(statusCode) from server: \(undocumentedPayload).")
}
}
I really like the switch
statement on the response and the body for type-safe evaluations.
I was surprised to see that my generated sources changed when I updated to Swift OpenAPI Generator 0.1.3. But such changes may not be avoidable until the project reaches its first major release.
I published my sample application on GitHub.