Meet Swift on Server
Build a service
이벤트와 이벤트 참여자를 목록화하는 서비스만들기
편집기 선택하기
필요한 패키지 정의하기(Package.swift)
openapi-generator와 vapor를 사용한다.
// 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"
),
]
),
]
)
swift openapi generator는 api를 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"
초기 서버 구현(정적 데이터만 반환)
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())
}
}
데이터 추가를 위해 Database driver 추가
예제에서는 Postgre를 사용할 것이다.
의존성 추가
// 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"
),
]
),
]
)
PostgresClient 추가
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())
}
}
createEvent 구현
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())
}
}
위 쿼리는 문자열처럼 보이지만 실제로는 Swift의 String interpolation이다.
let query = PostgresQuery(
"""
INSERT INTO events (name, date, attendee)
VALUES ($0, $1, $2)
"""
)
query.binds.append(event.name)
query.binds.append(event.date)
query.binds.append(event.attendee)
에러 로깅
Observability
Swift는 이 3가지 축에 대한 패키지를 모두 지원한다.
func listEvents(
_ input: Operations.listEvents.Input
) async throws -> Operations.listEvents.Output {
// swift-log로 로그 남기기
let logger = Logger(label: "ListEvents")
logger.info("Handling request", metadata: ["operation": "\\(Operations.listEvents.id)"])
// swift-metrics로 metric 남기기
Counter(label: "list.events.counter").increment()
// swift-distributed-tracing으로 요청 전체에서 쿼리 영역을 정의
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))))
}
}
이를 위해서는 앱 시작시 각 시스템들을 부트스트래핑 해야한다.
시작하자마자해야 이벤트 유실을 막을 수 있다.
log - metric - InstrumentationSystem 순으로 초기화하는 것을 권장한다.
@main
struct Service {
static func main() async throws {
LoggingSystem.bootstrap(StreamLogHandler.standardError)
let registry = PrometheusCollectorRegistry()
MetricsSystem.bootstrap(PrometheusMetricsFactory(registry: registry))
let otelTracer = OTel.Tracer(...)
InstrumentationSystem.bootstrap(otelTracer)
}
}
createEvent에 로그를 남기기
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())
}
}
}
Explore the ecosystem