Streaming is available in most browsers,
and in the Developer app.
-
Meet Swift OpenAPI Generator
Discover how Swift OpenAPI Generator can help you work with HTTP server APIs whether you're extending an iOS app or writing a server in Swift. We'll show you how this package plugin can streamline your workflow and simplify your codebase by generating code from an OpenAPI document.
Chapters
- 0:44 - Considerations when making API calls
- 1:52 - Meet OpenAPI
- 6:15 - Making API calls from your app
- 12:33 - Adapting as the API evolves
- 14:23 - Testing your app with mocks
- 16:12 - Server development in Swift
- 19:24 - Adding a new operation
Resources
- Swift OpenAPI Generator package plugin
- Swift OpenAPI Generator Runtime
- URLSession Transport for Swift OpenAPI Generator
Related Videos
WWDC22
-
Download
♪ ♪ Si: Hi, I'm Si from the Swift on Server team at Apple. In this video, I'll show you how Swift OpenAPI Generator can help you work with server APIs. Whether extending an iOS app or writing a back-end server in Swift, this new Swift package plug-in can streamline your workflow and simplify your codebase. This year, we've seen how it's easier than ever to work with data on the device. But sometimes the feature you want to implement will require dynamic content that's provided by a server component. This means making network requests to a remote service, calling its API. But in order to make the right network request, there's a lot to consider. What's the base URL of the server? What path components make up the API endpoint? What HTTP method should you use? And how should you provide parameters? These are just some of the questions to consider when calling a server API. For more involved APIs, you'll need to consider much more. So how do you answer these questions? Most services have some form of API documentation. But handwritten documentation can often be inaccurate or outdated, especially if the service is rapidly evolving. If you have access to the source code, you could look at the implementation, or you could manually experiment with the API. But this can lead to an incomplete understanding of the service behavior. You can search support forums or rely on other institutional knowledge. But even the most well-meaning individuals may be under-informed or provide inconsistent answers, leaving you with more questions than you started with. While these resources offer some help, it's not a complete picture. Using a more formal and structured description of APIs can help eliminate ambiguity. Meet OpenAPI, an open specification for defining HTTP services.
OpenAPI is an industry standard, and its widespread adoption and maturity means there are established conventions and best practices to help you work with APIs. With OpenAPI, you document the service behavior in either YAML or JSON, and these machine-readable formats allow you to benefit from a rich ecosystem of tooling. There's tools for test generation, runtime validation, interoperability, and much more. One thing that OpenAPI is particularly known for is tooling to generate interactive documentation. But the core motivation of OpenAPI is code generation which allows adopters to use spec-driven development. Remember our example API endpoint? Well, upon receiving this request, the server returns a personalized greeting in an JSON object. Let's take a look at the code we'd need to write to call this API, without using code generation.
First, we need to know the base URL of the server to convert into its components. Then, we append the path component to construct the API endpoint and specify the parameter as a query item. Then we construct a URLRequest and use URLSession to make the HTTP request.
Then we must validate the responses of the expected type has the expected status code and content type.
Then we must decode the bytes from the response, which we do by defining a Swift type that conforms to Decodable and using JSONDecoder. Finally, we return the message property from the response.
Writing this code is fine, but this was just a single request for a trivial API operation. Many real-world APIs have hundreds of operations, with rich request and response types, header fields, parameters, and more. Writing this code for every operation becomes repetitive, verbose, and error-prone. And all of this ceremony in your codebase detracts from the core logic of your app. With OpenAPI, you can use tooling to generate most of this code so you can focus on the code your users interact with. We'll use our example API to explore an OpenAPI document. Every OpenAPI document declares the version of the OpenAPI specification it's using. It provides metadata about the API, including its name and version and a list of server URLs. It then lists the paths and HTTP methods that make up the API. This API has just one operation, named getGreeting, which defines the behavior for the GET method on the greet path. In this example, the server always responds with 200, which is the HTTP status code for OK, and returns a JSON object, which is defined using JSON Schema.
For this illustration, we've kept it simple, but operations can have multiple responses, with different status codes and content types, which allows you to document all scenarios, including what happens when there's an error. And if the operation accepts parameters, these can also be included in the OpenAPI document.
This operation supports one optional query parameter called "name", a string value that's used to personalize the greeting.
With the help of Swift OpenAPI Generator, we can make the same API call with much less code.
We can use type-safe inputs and the output values are rich enum types so the compiler can help us ensure we handle every documented response and content type. And the associated value in the response body is a value type with type-safe properties. All the ceremony associated with encoding the input, making the request, parsing the response, and decoding the output is all handled by generated code.
Swift OpenAPI Generator is a Swift Package plug-in which runs at build time. This means the generated code is always in sync with the OpenAPI document and doesn't need to be committed to your source repository. To learn more about Swift package plug-ins, check out the session named "Meet Swift package plugins." Let's take a look at how we can use Swift OpenAPI Generator in a simple iOS app. For this, we'll need an API that we can call. In this demo, we'll call a simple API that returns one of the ten cat face emojis at random. We'll start with the template SwiftUI app and replace the sample content with a big emoji and a button which fetches a new one from the server on each tap. We already have a server running, listening on localhost, which we can query from the Terminal using curl.
There's no denying that this is a great API. But what makes it even better is that it's defined using OpenAPI. Let's use a very different kind of cat to show the OpenAPI document for this service.
This API has a single operation named getEmoji which we'll call from our app to update the UI. To get started, we'll switch over to Xcode.
This sample iOS app has a basic UI, defined using SwiftUI, which we can see in the Xcode preview. In the next few minutes, we'll replace the UI components with dynamic content, which we'll fetch from the server. And we'll use Swift OpenAPI Generator to simplify the code we have to write by hand to make the API calls. We'll start by adding the required package dependencies to our project. Then we'll configure our target to use the plugin for code generation and add the OpenAPI document and plug-in config file to our target source directory. Once the project is configured, we'll replace the UI components and use the generated Client type to make the API calls to the server. To configure your app to use Swift OpenAPI Generator, navigate to the Project Editor, select the Package Dependencies tab, and click to add a new dependency.
For this demo, we're using a local package collection, but you can find the package URLs in the session notes. First, we'll add a dependency on swift-openapi-generator, which provides the package plug-in.
Then we'll add a dependency on swift-openapi-runtime, which provides the common types and abstractions used by the generated code.
And because the generated code isn't tied to any specific HTTP client library, we need to choose an integration package for the library we'd like to use. We're building an iOS app, so we'll use the URLSession package, but check out the documentation for other examples and how to write your own. With the dependencies in place, we can configure the target to use the OpenAPI Generator plugin. In Target Settings, select Build Phases and expand the section named "Run Build Tool Plug-ins". Click to add a new plug-in and select OpenAPIGenerator from the list.
The plug-in expects two input files in your target source directory: the OpenAPI document and a plug-in config file, which I'll add to the project now.
The plug-in configuration is written using a simple YAML schema which specifies what code the plug-in should generate. In this case, we'll generate "types", which are the reusable types derived from the OpenAPI document. And we'll also generate the client code, which can be used to make API calls with any HTTP client. We'll switch back to ContentView.swift, which will recompile our project so the generated code is ready to use in our app.
As a security measure, you'll be asked to trust the plug-in the first time you use it.
Now we've recompiled the project, we can replace the UI components and use the generated Client type to make API calls to the server and update the view.
We'll start by adding a new state property to the view for our emoji and initialize it with a placeholder value. Then, we'll replace the globe image with a Text view containing the emoji, replace the "Hello, world" message with a button, and set the button style for our view.
The generated code provides a type, named Client, that you can use to make API calls. But first, we need to import the OpenAPI runtime and transport modules.
Now we can add a client property to our view and an initializer which configures it to use the localhost server URL, defined in the OpenAPI document.
Now we'll add a function that makes an API call to the server using this client.
That's all the code we need to write by hand to make the API request. Everything else is handled by the generated code. The response is an enum value of a type that models all the documented responses and content types which encourages us to handle all scenarios. So we need to extract the emoji from the response body using a switch statement.
Something's missing here. The compiler has told us we haven't handled every scenario. We'll let Xcode fill in the missing switch case.
In the event the server responds with something that isn't specified in its OpenAPI document, you still have a chance to handle that gracefully. For this demo, we'll print a warning to the console and update our emoji to something other than a cat.
Now we can call this function when our button is tapped.
And we can use our button to fetch a new cat emoji and update our UI.
As new features are added to the server, its API will evolve. And if the server is documented using OpenAPI, then Swift OpenAPI Generator makes it simple to use these new features from your app. Let's walk through an example of how to update the app as the OpenAPI document evolves.
When it comes to emojis, more is more, so we've extended the service API to take a new optional query parameter, count, which can be used to fetch multiple emojis.
We'll extend our app with another button that fetches three cats instead of one.
First, we'll add a parameter to the OpenAPI document. And once we recompile the project, the parameter will be available to use in the app. Then we'll create a new button that makes an API call using this parameter. We'll start by adding the new parameter to the OpenAPI document.
This parameter is named "count". It's an optional parameter. It's provided as part of the URL query and is an integer value. Let's head back to ContentView.swift and extend the updateEmoji function to also take a parameter.
And let's use this parameter when making the API call.
We'll duplicate the button and change the label to "More cats".
When this button is tapped, we'll call the same function, but this time with a count of three.
Now in the preview, we can tap “Get cat" to get one cat or “More cats" to get three. All this time, we've been making requests to a real server, which isn't always practical or desirable, especially during development. Because the generated Client type conforms to a Swift protocol, it's easy for us to write a mock that requires no network connection or transport library. The generated protocol is named APIProtocol, so we'll start by defining a new MockClient type that adopts this protocol. Then we'll update our view to be generic over any type that conforms to APIProtocol and update the initializers to support dependency injection. Then we'll use the MockClient when previewing the UI in Xcode. We'll start by declaring our MockClient type.
Because we declared that this type adopts APIProtocol, the compiler will ensure it satisfies the protocol requirements. We'll let Xcode add the missing handler for the API operation.
And we'll add the business logic, which returns robot emojis, to distinguish it from the real service.
Now we can make our view generic over types that conform to this protocol and update the client property to use the generic type parameter.
We'll add an initializer, which takes a client as a parameter, and we'll update the existing initializer with a generic where clause, so if no client is provided, we'll use the same one as before.
When our app is launched, it will continue to use the real server, but now we can inject the MockClient when previewing the UI in Xcode.
Now when we tap our buttons in the UI preview, we'll get robots instead of cats and won't require a network connection or a running server.
Until we added the mock client, our iOS app was making requests to a real server running on my local machine. This server was also written in Swift with the help of Swift OpenAPI Generator. The server is a simple Swift package, which uses the Swift OpenAPI Generator package plug-in for code generation. To use the generated server code, we defined a type conforming to the generated protocol named APIProtocol and implemented just the business logic for our API operations. And to configure the server, we used a generated function, registerHandlers, which connects the incoming HTTP requests for the API operations to our handlers that provide the business logic. Let's take a look.
If we expand the console, we can see the actual requests from our demo iOS app.
And this is all the Swift code we needed to write by hand to implement the server. Instead of using OpenAPI to just document this service, we started with the OpenAPI document and used Swift OpenAPI Generator to simplify writing a server that implements the API specification.
We have defined a type that conforms to the generated APIProtocol and provides just the business logic for our API operations. And we used a generated function, to register its methods with the HTTP server for the API endpoints. In this demo, we're using Vapor, an open source web framework for Swift. But the generated code can be used with any web framework that provides an integration package for Swift OpenAPI Generator. Check out the documentation for other options and how you can write your own.
In our main function, we create a new Vapor application, which we use to create an OpenAPI transport. Then we create an instance of our handler type and use the generated registerHandlers function to set up the routing within the HTTP server for each of our API operations, which we'd otherwise have to do manually. Finally, we run the Vapor app, in the same way as if we had manually configured it.
Swift is a great language for server development, and if you'd like to learn more about writing back-end services in Swift, check out the session named "Use Xcode for server-side development." Let's take a look at how the package is configured to use Swift OpenAPI Generator. The server is implemented as a Swift package and is defined using a Package.swift file. This package has a single executable target, called CatService, which makes use of the Swift OpenAPI Generator plugin. The generated server code depends on common types and abstractions from the runtime library and can be used with any web framework that provides an integration package, so this target has dependencies on swift-openapi-runtime, swift-openapi-vapor, and vapor itself. In the target source directory, we added the OpenAPI document, which is the same one we used in our demo iOS app, and the plug-in config file. For this target, we're generating the types and the server stubs. Let's see how spec-driven development can make it simpler to add new features to this service.
Cat emojis are a great, but lots of evidence suggests that the internet was primarily built for the exchange of cat videos, so we'll add that feature to our server.
With spec-driven development, adding a new API operation requires just two steps. First, we add the new operation to the OpenAPI document. Then, because the generated protocol now has a new function requirement, the compiler will insist we define a method on our handler and implement the business logic. Before we start, we'll need a cat video, which I've added to the Resources folder for our target.
We'll head over to the OpenAPI document and add the new operation.
This operation is called getClip and has a binary response with a content type indicating the response body contains video data.
When we try to recompile our package, it will fail.
That's because our handler no longer conforms to the generated protocol, because it doesn't provide a function for the new operation. We'll let Xcode fill in a protocol stub for us and we'll provide the business logic, which reads the bytes from the video resource file and returns an OK response, with a binary body. Note the type-safe generated code only allows returning a binary response body from this function, because that's what's specified in the OpenAPI document for this operation. When we recompile our package, it'll succeed and we can relaunch our server. And if we switch over to Safari, we can test our new API endpoint.
So we've seen how documenting services using OpenAPI can help eliminate ambiguity and enable spec-driven development. We've shown how Swift OpenAPI Generator can simplify working with server APIs in your iOS app. Finally, we've seen how Swift's language features and the growing Swift-on-server ecosystem make it a great choice for implementing back-end services. And that's why Swift OpenAPI Generator is open source and available on GitHub, where you can learn more and even contribute to the project as it continues to grow. Thanks for watching this session. That's all for meow!
-
-
4:17 - Example OpenAPI document
openapi: "3.0.3" info: title: "GreetingService" version: "1.0.0" servers: - url: "http://localhost:8080/api" description: "Production" paths: /greet: get: operationId: "getGreeting" parameters: - name: "name" required: false in: "query" description: "Personalizes the greeting." schema: type: "string" responses: "200": description: "Returns a greeting" content: application/json: schema: $ref: "#/components/schemas/Greeting"
-
7:05 - CatService openapi.yaml
openapi: "3.0.3" info: title: CatService version: 1.0.0 servers: - url: http://localhost:8080/api description: "Localhost cats 🙀" paths: /emoji: get: operationId: getEmoji parameters: - name: count required: false in: query description: "The number of cats to return. 😽😽😽" schema: type: integer responses: '200': description: "Returns a random emoji, of a cat, ofc! 😻" content: text/plain: schema: type: string
-
8:03 - Making API calls from your app
import SwiftUI import OpenAPIRuntime import OpenAPIURLSession #Preview { ContentView() } struct ContentView: View { @State private var emoji = "🫥" var body: some View { VStack { Text(emoji).font(.system(size: 100)) Button("Get cat!") { Task { try? await updateEmoji() } } } .padding() .buttonStyle(.borderedProminent) } let client: Client init() { self.client = Client( serverURL: try! Servers.server1(), transport: URLSessionTransport() ) } func updateEmoji() async throws { let response = try await client.getEmoji(Operations.getEmoji.Input()) switch response { case let .ok(okResponse): switch okResponse.body { case .text(let text): emoji = text } case .undocumented(statusCode: let statusCode, _): print("cat-astrophe: \(statusCode)") emoji = "🙉" } } }
-
9:48 - CatServiceClient openapi-generator-config.yaml
generate: - types - client
-
13:24 - Adapting as the API evolves
import SwiftUI import OpenAPIRuntime import OpenAPIURLSession #Preview { ContentView() } struct ContentView: View { @State private var emoji = "🫥" var body: some View { VStack { Text(emoji).font(.system(size: 100)) Button("Get cat!") { Task { try? await updateEmoji() } } Button("More cats!") { Task { try? await updateEmoji(count: 3) } } } .padding() .buttonStyle(.borderedProminent) } let client: Client init() { self.client = Client( serverURL: try! Servers.server1(), transport: URLSessionTransport() ) } func updateEmoji(count: Int = 1) async throws { let response = try await client.getEmoji(Operations.getEmoji.Input( query: Operations.getEmoji.Input.Query(count: count) )) switch response { case let .ok(okResponse): switch okResponse.body { case .text(let text): emoji = text } case .undocumented(statusCode: let statusCode, _): print("cat-astrophe: \(statusCode)") emoji = "🙉" } } }
-
15:06 - Testing your app with mocks
import SwiftUI import OpenAPIRuntime import OpenAPIURLSession #Preview { ContentView(client: MockClient()) } struct ContentView<C: APIProtocol>: View { @State private var emoji = "🫥" var body: some View { VStack { Text(emoji).font(.system(size: 100)) Button("Get cat!") { Task { try? await updateEmoji() } } Button("More cats!") { Task { try? await updateEmoji(count: 3) } } } .padding() .buttonStyle(.borderedProminent) } let client: C init(client: C) { self.client = client } init() where C == Client { self.client = Client( serverURL: try! Servers.server1(), transport: URLSessionTransport() ) } func updateEmoji(count: Int = 1) async throws { let response = try await client.getEmoji(Operations.getEmoji.Input( query: Operations.getEmoji.Input.Query(count: count) )) switch response { case let .ok(okResponse): switch okResponse.body { case .text(let text): emoji = text } case .undocumented(statusCode: let statusCode, _): print("cat-astrophe: \(statusCode)") emoji = "🙉" } } } struct MockClient: APIProtocol { func getEmoji(_ input: Operations.getEmoji.Input) async throws -> Operations.getEmoji.Output { let count = input.query.count ?? 1 let emojis = String(repeating: "🤖", count: count) return .ok(Operations.getEmoji.Output.Ok( body: .text(emojis) )) } }
-
16:58 - Implementing a backend server
import Foundation import OpenAPIRuntime import OpenAPIVapor import Vapor struct Handler: APIProtocol { func getEmoji(_ input: Operations.getEmoji.Input) async throws -> Operations.getEmoji.Output { let candidates = "🐱😹😻🙀😿😽😸😺😾😼" let chosen = String(candidates.randomElement()!) let count = input.query.count ?? 1 let emojis = String(repeating: chosen, count: count) return .ok(Operations.getEmoji.Output.Ok(body: .text(emojis))) } } @main struct CatService { public static func main() throws { let app = Vapor.Application() let transport = VaporTransport(routesBuilder: app) let handler = Handler() try handler.registerHandlers(on: transport, serverURL: Servers.server1()) try app.run() } }
-
18:43 - CatService Package.swift
// swift-tools-version: 5.8 import PackageDescription let package = Package( name: "CatService", platforms: [ .macOS(.v13), ], dependencies: [ .package( url: "https://github.com/apple/swift-openapi-generator", .upToNextMinor(from: "0.1.0") ), .package( url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.1.0") ), .package( url: "https://github.com/swift-server/swift-openapi-vapor", .upToNextMinor(from: "0.1.0") ), .package( url: "https://github.com/vapor/vapor", .upToNextMajor(from: "4.69.2") ), ], targets: [ .executableTarget( name: "CatService", dependencies: [ .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"), .product(name: "OpenAPIVapor", package: "swift-openapi-vapor"), .product(name: "Vapor", package: "vapor"), ], resources: [ .process("Resources/cat.mp4"), ], plugins: [ .plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator"), ] ), ] )
-
19:08 - CatService openapi.yaml
openapi: "3.0.3" info: title: CatService version: 1.0.0 servers: - url: http://localhost:8080/api description: "Localhost cats 🙀" paths: /emoji: get: operationId: getEmoji parameters: - name: count required: false in: query description: "The number of cats to return. 😽😽😽" schema: type: integer responses: '200': description: "Returns a random emoji, of a cat, ofc! 😻" content: text/plain: schema: type: string
-
19:10 - CatService openapi-generator-config.yaml
generate: - types - server
-
20:11 - Adding an operation to the OpenAPI document
openapi: "3.0.3" info: title: CatService version: 1.0.0 servers: - url: http://localhost:8080/api description: "Localhost cats 🙀" paths: /emoji: get: operationId: getEmoji parameters: - name: count required: false in: query description: "The number of cats to return. 😽😽😽" schema: type: integer responses: '200': description: "Returns a random emoji, of a cat, ofc! 😻" content: text/plain: schema: type: string /clip: get: operationId: getClip responses: '200': description: "Returns a cat video! 😽" content: video/mp4: schema: type: string format: binary
-
20:26 - Adding a new API operation
import Foundation import OpenAPIRuntime import OpenAPIVapor import Vapor struct Handler: APIProtocol { func getClip(_ input: Operations.getClip.Input) async throws -> Operations.getClip.Output { let clipResourceURL = Bundle.module.url(forResource: "cat", withExtension: "mp4")! let clipData = try Data(contentsOf: clipResourceURL) return .ok(Operations.getClip.Output.Ok(body: .binary(clipData))) } func getEmoji(_ input: Operations.getEmoji.Input) async throws -> Operations.getEmoji.Output { let candidates = "🐱😹😻🙀😿😽😸😺😾😼" let chosen = String(candidates.randomElement()!) let count = input.query.count ?? 1 let emojis = String(repeating: chosen, count: count) return .ok(Operations.getEmoji.Output.Ok(body: .text(emojis))) } } @main struct CatService { public static func main() throws { let app = Vapor.Application() let transport = VaporTransport(routesBuilder: app) let handler = Handler() try handler.registerHandlers(on: transport, serverURL: Servers.server1()) try app.run() } }
-
-
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.