Streaming is available in most browsers,
and in the Developer app.
-
Explore the Swift on Server ecosystem
Swift is a great language for writing your server applications, and powers critical services across Apple's cloud products. We'll explore tooling, delve into the Swift server package ecosystem, and demonstrate how to interact with databases and add observability to applications.
Chapters
- 0:00 - Introduction
- 0:13 - Agenda
- 0:27 - Meet Swift on Server
- 2:30 - Build a service
- 3:46 - Swift OpenAPI generator
- 5:42 - Database drivers
- 10:53 - Observability
- 15:19 - Explore the ecosystem
- 16:12 - Wrap up
Resources
Related Videos
WWDC23
-
Download
Hi, I'm Franz from the Swift on Server Team at Apple. Today, we are going to explore the Swift on Server ecosystem. First, we will talk about why Swift is such a great language for developing server applications. Then we are going to build a service with some popular packages from the ecosystem. Finally, we will explore how the ecosystem operates and where to learn more. Let's get started on what makes Swift great for server applications. Swift let's you achieve C-like performance with a low memory footprint due to automatic reference counting instead of garbage collection. Making it a perfect fit for modern cloud services which require a predictable resource consumption and fast startup time. Swift is an expressive and safe language, which eliminates a range of bugs at compile time, allowing developers to write robust and reliable distributed systems. Features such as strong-typing, optionals and memory safety make Swift services less prone to crashes and security vulnerabilities. Cloud services often have to handle highly concurrent workloads. Swift's first class concurrency features allow developers to write scalable and responsive server applications while eliminating a common source of bugs due to data races. All these properties make Swift an excellent choice for writing server applications. In fact, Swift powers many critical features across Apple's cloud services such as iCloud Keychain, Photos and Notes. Other use cases include, the App Store processing pipelines and SharePlay file sharing. Lastly, our brand new Private Cloud Compute service is built using Swift on Server. Across Apple's services this amounts to millions of requests per second being handled by applications using Swift on Server.
Outside of Apple's platforms, the Server ecosystem was one of the earliest users of Swift. In fact, the Swift Server workgroup was founded in 2016, just one year after the open sourcing of Swift. Making it the oldest workgroup. The workgroup consists of members representing companies using Swift on Server and individual contributors of the ecosystem. It is focused on promoting the use of Swift for developing and deploying server applications. The workgroup's responsibilities include: defining and prioritising efforts to address the needs of the server community; running an incubation process for packages to reduce duplication of effort, increase compatibility and promote best practices. The workgroup also channels feedback from the server ecosystem back to the other groups of the Swift project. Now, let's use some of the popular packages from the server ecosystem to build a service. My colleagues and I are planning to attend a lot of events this year. To organise ourselves we wanted to implement an event service that keeps track of who is attending which event. The service should support two operations. One to list all events to show who is planning to attend what event. And since I can't miss going to the Oktoberfest we need another operation to create a new event. To work on Swift packages you can use different editors such as Xcode, VS Code, Neovim, or any other editor that supports the language server protocol. For today's demo, we are going to use VS Code to work on our package. During the demo we are going to use the built-in terminals from VS Code at the bottom to see the outputs from our service and send requests to it. I already prepared a package for us to get started on the event service. Let's go check it out. The package depends on the OpenAPI generator and uses Vapor as the server transport for OpenAPI.
The package has two targets. The EventAPI target, which has the OpenAPIGenerator plugin configured and the EventService executableTarget which contains the implementation of our service. Swift OpenAPI Generator allows us to document our service in YAML and generate code for servers and clients. If you are new or want to review OpenAPI feel free to watch "Meet Swift OpenAPI Generator" from last year. Let's review the OpenAPI document. It defines both of our operations in the events path The first operation is a get method called listEvents.
The operation returns a success response containing an array of events.
The second operation is a post method called createEvent.
This operation takes a JSON body of an event and depending if the creation was successful the operation returns a 201 or a 400 status code.
Our Service contains the main entry point. We first create a Vapor application.
Then we create the OpenAPI VaporTransport with the application.
Next, we create an instance of our Service and register it on the transport.
And lastly, we execute the Vapor application, which will start an HTTP server and listen for incoming connections.
Our Service also implements the generated APIProtocol.
The listEvents method returns a hard coded array of events.
The createEvent method is currently returning a not implemented status code.
Let's go ahead and start our service.
This is going to build our service and attach a debugger to it. At the bottom in the terminal we can see that the server started. Now we can list all events by querying our service in another terminal using curl.
Great, our service returned a JSON array containing the hard coded list of events. However, we want to dynamically add new events and persist them in a database, so let's take a look at database drivers. There are many different database drivers in the open source ecosystem for databases like PostgreSQL, MySQL, Cassandra, MongoDB and many more.
Today we are going to use a Postgres database for persistence. PostgresNIO is an open source database driver for Postgres maintained by Vapor and Apple. New in PostgresNIO 1.21 is the PostgresClient. The PostgresClient provides a completely new asynchronous interface and comes with a built-in connection pool which leverages structured concurrency, making it resilient against intermittent networking failures to the database. Additionally, the connection pool improves throughput by distributing queries over multiple connections and prewarming connections for faster query execution. Let's go ahead and use PostgresNIO to connect our EventService to a database. We'll start by adding a dependency to PostgresNIO in our package and import it in our service. Then we are going to use the PostgresClient to query our database in the listEvents method. Lastly, we are going to implement the createEvent method to insert the new event into the database. Let's start by adding the dependency to PostgresNIO in our package manifest.
Then we can add a dependency to our EventService target.
Now we can import PostgresNIO in our Service.
Next we are going add a PostgresClient property to our service.
We'll use the client to query the database in the listEvents method The query method returns an AsyncSequence of rows. To replace the hard coded list of events we'll iterate over the rows, decode the fields and create an event for each row.
The AsyncSequence returned by the query method will automatically prefetch rows from the database to speed up performance. Before we can run our service again we have to create a PostgresClient and pass it to our Service.
First, we create a PostgresClient to connect to a database that I already started locally.
Next we are going to pass the PostgresClient to our service.
To start the client we need to call its run method, which is going to take over the current task until it is finished. Since we want to run both the Vapor application and the PostgresClient concurrently we are going to use a task group. We'll create a discarding task group and add a child task that runs the PostgresClient.
Then we move the Vapor application execution into a separate child task.
Let's run our service again.
The restart button is going to stop the current process, rebuild the service and start it again.
The terminal at the bottom shows it's running. Let's list all events again.
Our database appears to be empty. To add new events to the database we'll implement the createEvent method next.
First, we have to switch over the input and extract the JSON event.
Then we are going to query the database to insert the new event.
Lastly, we have to return that we created the event.
Seeing this code might set off alarms for some people since in other languages this is a common vector for SQL injection vulnerabilities. Even though this looks like a string it isn't a string, but uses Swift's String interpolation feature to transform the string query into a parameterised query with value binding. Making it completely safe from SQL injection attacks. This is a great example of Swift's commitment to making things ergonomic while guaranteeing the safety of the code. We are going to restart our service.
Once the service is running again we are going to use curl to create two events.
Looks like the event creation was successful. Let's check if the events have been stored in our database by listing all events again.
Perfect, all events were saved in the database. Gus just send me a message that he wanted to bring a friend and asked if I can add another event entry under his name. Let's go ahead and create another event for Gus.
Looks like something went wrong when trying to add another event entry under Gus's name. In the terminal at the bottom we can see a long error message; however, the error isn't telling us exactly what went wrong. The only information that we see is that the operation couldn't be completed and that the thrown error was of the type PSQLError. The description of PSQLError intentionally omits detailed information to prevent accidental leakage of database information such as the schemas of your table. In cases like this, adding additional observability to your service helps troubleshooting. Observability consists of three pillars logging, metrics and tracing. Logging helps you understand exactly what a service did and lets you dig into the details while troubleshooting problems. Metrics allow you to get a high level overview of the health of your service at a glance. While logs and metrics help you understand what a single service does modern cloud systems are often a collection of distributed systems. This is where tracing helps you understand what path a single request took through your system. The Swift ecosystem offers API packages for all three pillars that allow code to emit observability events. Let's take a look how we could instrument our listEvents method. First, we can use swift-log to emit a log when we start to handle a new listEvents request. swift-log supports structured logging by adding metadata to log messages providing additional context when troubleshooting problems. Next, we can add a counter from swift-metrics that we increment on each request to track how many requests our service has processed. Lastly, we can add swift-distributed-tracing to create a span around our database query, which can help while troubleshooting a request end to end through our system. If you want to learn more about how distributed tracing in Swift works check out last year's session "Beyond the basics of structured concurrency". We have just instrumented our listEvents method with logging, metrics and tracing. The APIs we used are observability backend agnostic leaving the choice to the service author where to send the data. The Swift on Server ecosystem contains many different backends for logging, metrics and distributed tracing. Choosing the backends is done by calling the bootstrapping methods of the three libraries. Bootstrapping should only be done in executables and should happen as early as possible to ensure no observability event is lost. Additionally, it is recommended to bootstrap your LoggingSystem first, then your MetricsSystem, and lastly your InstrumentationSystem. This is because metrics and instrumentation systems might want to emit logs about their status. With just a couple of lines of code we were able to emit logs to the terminal, metrics to Prometheus and traces to Open Telemetry. Let's add logging to our createEvent method to understand what exactly goes wrong when we try to add another event under Gus' name. First, we have to add swift-log as a dependency to our package and our EventService target.
Then we can import the Logging module in our Service.
Next, we'll catch the errors thrown by the query method.
The query method throws a PSQLError in the case something went wrong when executing the query.
Let's create a logger so that we can emit a log event containing the error message sent by the Postgres server.
Next, we are going to extract the error message and emit the log. The PSQLError contains detailed information about what went wrong in the serverInfo property.
Last, we'll return a badRequest response to indicate something went wrong while adding the event to the database.
Let's restart our service and see if we can get more details about the error.
By default swift-log will emit logs to the terminal. This is perfect for debugging our application. We'll run the same curl command to create the event again.
This time we didn't get the same error since we returned the badRequest status code. So let's checkout the logs from our service to see what went wrong. In the terminal at the bottom we can see the log message. The error message metadata field tells us that the error was due to a duplicate key violation. Our database table only allows a single entry for a combination of name, date and attendee. Adding logging helped us troubleshoot the concrete problem. I'll let my colleague fix that bug later.
This was just a short glimpse at some of the libraries of the Swift on Server ecosystem that you can use to build a service. There are many more libraries for various use cases such as networking, database drivers, observability, message streaming, and much more. If you are interested in finding more libraries go to the swift.org packages section and explore the server category. You can also use the swift package index to find more server libraries. Another great resource to find packages is the incubation list of the Swift Server Workgroup. The workgroup runs an incubation process for packages to create a robust and stable ecosystem. Packages in the incubation process transition through maturity levels from Sandbox, to Incubating, to Graduated. Each level has different requirements aligned with the package's production readiness and usage. You can find the list of incubated packages on swift.org I hope this session excited you about the Swift on Server ecosystem. We talked about why Swift is such a great language for server applications and how it is powering a lot of critical features across Apple's cloud services. We also explored some of the packages and how the Swift Server Workgroup helps to grow a healthy ecosystem. Thank you for watching! Servus and see you at the Oktoberfest!
-
-
3:23 - EventService Package.swift
// swift-tools-version:5.9 import PackageDescription let package = Package( name: "EventService", platforms: [.macOS(.v14)], dependencies: [ .package( url: "https://github.com/apple/swift-openapi-generator", from: "1.2.1" ), .package( url: "https://github.com/apple/swift-openapi-runtime", from: "1.4.0" ), .package( url: "https://github.com/vapor/vapor", from: "4.99.2" ), .package( url: "https://github.com/swift-server/swift-openapi-vapor", from: "1.0.1" ), ], targets: [ .target( name: "EventAPI", dependencies: [ .product( name: "OpenAPIRuntime", package: "swift-openapi-runtime" ), ], plugins: [ .plugin( name: "OpenAPIGenerator", package: "swift-openapi-generator" ) ] ), .executableTarget( name: "EventService", dependencies: [ "EventAPI", .product( name: "OpenAPIRuntime", package: "swift-openapi-runtime" ), .product( name: "OpenAPIVapor", package: "swift-openapi-vapor" ), .product( name: "Vapor", package: "vapor" ), ] ), ] )
-
4:05 - EventService openapi.yaml
openapi: "3.1.0" info: title: "EventService" version: "1.0.0" servers: - url: "https://localhost:8080/api" description: "Example service deployment." paths: /events: get: operationId: "listEvents" responses: "200": description: "A success response with all events." content: application/json: schema: type: "array" items: $ref: "#/components/schemas/Event" post: operationId: "createEvent" requestBody: description: "The event to create." required: true content: application/json: schema: $ref: '#/components/schemas/Event' responses: '201': description: "A success indicating the event was created." '400': description: "A failure indicating the event wasn't created." components: schemas: Event: type: "object" description: "An event." properties: name: type: "string" description: "The event's name." date: type: "string" format: "date" description: "The day of the event." attendee: type: "string" description: "The name of the person attending the event." required: - "name" - "date" - "attendee"
-
4:35 - EventService initial implementation
import OpenAPIRuntime import OpenAPIVapor import Vapor import EventAPI @main struct Service { static func main() async throws { let application = try await Vapor.Application.make() let transport = VaporTransport(routesBuilder: application) let service = Service() try service.registerHandlers( on: transport, serverURL: URL(string: "/api")! ) try await application.execute() } } extension Service: APIProtocol { func listEvents( _ input: Operations.listEvents.Input ) async throws -> Operations.listEvents.Output { let events: [Components.Schemas.Event] = [ .init(name: "Server-Side Swift Conference", date: "26.09.2024", attendee: "Gus"), .init(name: "Oktoberfest", date: "21.09.2024", attendee: "Werner"), ] return .ok(.init(body: .json(events))) } func createEvent( _ input: Operations.createEvent.Input ) async throws -> Operations.createEvent.Output { return .undocumented(statusCode: 501, .init()) } }
-
6:56 - EventService Package.swift
// swift-tools-version:5.9 import PackageDescription let package = Package( name: "EventService", platforms: [.macOS(.v14)], dependencies: [ .package( url: "https://github.com/apple/swift-openapi-generator", from: "1.2.1" ), .package( url: "https://github.com/apple/swift-openapi-runtime", from: "1.4.0" ), .package( url: "https://github.com/vapor/vapor", from: "4.99.2" ), .package( url: "https://github.com/swift-server/swift-openapi-vapor", from: "1.0.1" ), .package( url: "https://github.com/vapor/postgres-nio", from: "1.19.1" ), ], targets: [ .target( name: "EventAPI", dependencies: [ .product( name: "OpenAPIRuntime", package: "swift-openapi-runtime" ), ], plugins: [ .plugin( name: "OpenAPIGenerator", package: "swift-openapi-generator" ) ] ), .executableTarget( name: "EventService", dependencies: [ "EventAPI", .product( name: "OpenAPIRuntime", package: "swift-openapi-runtime" ), .product( name: "OpenAPIVapor", package: "swift-openapi-vapor" ), .product( name: "Vapor", package: "vapor" ), .product( name: "PostgresNIO", package: "postgres-nio" ), ] ), ] )
-
7:08 - Implementing the listEvents method
import OpenAPIRuntime import OpenAPIVapor import Vapor import EventAPI import PostgresNIO @main struct Service { let postgresClient: PostgresClient static func main() async throws { let application = try await Vapor.Application.make() let transport = VaporTransport(routesBuilder: application) let postgresClient = PostgresClient( configuration: .init( host: "localhost", username: "postgres", password: nil, database: nil, tls: .disable ) ) let service = Service(postgresClient: postgresClient) try service.registerHandlers( on: transport, serverURL: URL(string: "/api")! ) try await withThrowingDiscardingTaskGroup { group in group.addTask { await postgresClient.run() } group.addTask { try await application.execute() } } } } extension Service: APIProtocol { func listEvents( _ input: Operations.listEvents.Input ) async throws -> Operations.listEvents.Output { let rows = try await self.postgresClient.query("SELECT name, date, attendee FROM events") var events = [Components.Schemas.Event]() for try await (name, date, attendee) in rows.decode((String, String, String).self) { events.append(.init(name: name, date: date, attendee: attendee)) } return .ok(.init(body: .json(events))) } func createEvent( _ input: Operations.createEvent.Input ) async throws -> Operations.createEvent.Output { return .undocumented(statusCode: 501, .init()) } }
-
9:02 - Implementing the createEvent method
func createEvent( _ input: Operations.createEvent.Input ) async throws -> Operations.createEvent.Output { switch input.body { case .json(let event): try await self.postgresClient.query( """ INSERT INTO events (name, date, attendee) VALUES (\(event.name), \(event.date), \(event.attendee)) """ ) return .created(.init()) } }
-
11:34 - Instrumenting the listEvents method
func listEvents( _ input: Operations.listEvents.Input ) async throws -> Operations.listEvents.Output { let logger = Logger(label: "ListEvents") logger.info("Handling request", metadata: ["operation": "\(Operations.listEvents.id)"]) Counter(label: "list.events.counter").increment() return try await withSpan("database query") { span in let rows = try await postgresClient.query("SELECT name, date, attendee FROM events") return try await .ok(.init(body: .json(decodeEvents(rows)))) } }
-
13:14 - EventService Package.swift
// swift-tools-version:5.9 import PackageDescription let package = Package( name: "EventService", platforms: [.macOS(.v14)], dependencies: [ .package( url: "https://github.com/apple/swift-openapi-generator", from: "1.2.1" ), .package( url: "https://github.com/apple/swift-openapi-runtime", from: "1.4.0" ), .package( url: "https://github.com/vapor/vapor", from: "4.99.2" ), .package( url: "https://github.com/swift-server/swift-openapi-vapor", from: "1.0.1" ), .package( url: "https://github.com/vapor/postgres-nio", from: "1.19.1" ), .package( url: "https://github.com/apple/swift-log", from: "1.5.4" ), ], targets: [ .target( name: "EventAPI", dependencies: [ .product( name: "OpenAPIRuntime", package: "swift-openapi-runtime" ), ], plugins: [ .plugin( name: "OpenAPIGenerator", package: "swift-openapi-generator" ) ] ), .executableTarget( name: "EventService", dependencies: [ "EventAPI", .product( name: "OpenAPIRuntime", package: "swift-openapi-runtime" ), .product( name: "OpenAPIVapor", package: "swift-openapi-vapor" ), .product( name: "Vapor", package: "vapor" ), .product( name: "PostgresNIO", package: "postgres-nio" ), .product( name: "Logging", package: "swift-log" ), ] ), ] )
-
13:38 - Adding logging to the createEvent method
func createEvent( _ input: Operations.createEvent.Input ) async throws -> Operations.createEvent.Output { switch input.body { case .json(let event): do { try await self.postgresClient.query( """ INSERT INTO events (name, date, attendee) VALUES (\(event.name), \(event.date), \(event.attendee)) """ ) return .created(.init()) } catch let error as PSQLError { let logger = Logger(label: "CreateEvent") if let message = error.serverInfo?[.message] { logger.info( "Failed to create event", metadata: ["error.message": "\(message)"] ) } return .badRequest(.init()) } } }
-
-
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.