Backend(アプリケーション層)から素のVapor依存を排除し、swift-api-serverが提供する抽象レイヤーのみを使用する設計に移行する。
Backend/Server
├── import Vapor ← 直接依存(問題)
├── configure.swift → Application, Environment, CORSMiddleware
├── routes.swift → RoutesBuilder, app.get(), app.grouped()
├── entrypoint.swift → @main, Environment.detect()
└── Shared+Content.swift → Content protocol conformance
┌─────────────────────────────────────┐
│ Backend/Server (Application Code) │
│ - No Vapor imports │
│ - Uses APIServer abstractions only │
└────────────────┬────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ swift-api-server │
│ ┌─────────────────────────────────┐ │
│ │ Public API (Abstractions) │ │
│ │ - ServerApplication │ │
│ │ - ServerRequest/Response │ │
│ │ - ServerMiddleware │ │
│ │ - RouteBuilder │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ Internal (Vapor Implementation) │ │
│ │ - VaporApplication │ │
│ │ - VaporRequest/Response │ │
│ │ - Vapor middleware adapters │ │
│ └─────────────────────────────────┘ │
└────────────────┬────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Vapor (Hidden dependency) │
└─────────────────────────────────────┘
// Public API
public protocol ServerApplication: Sendable {
associatedtype Routes: RouteRegistrar
var logger: ServerLogger { get }
var routes: Routes { get }
func middleware(_ middleware: any ServerMiddleware)
func run() async throws
func shutdown() async throws
}
// Factory for creating applications
public struct Server {
public static func application(
environment: ServerEnvironment = .detect()
) async throws -> some ServerApplication
}
// Environment abstraction
public enum ServerEnvironment: Sendable {
case development
case testing
case production
public static func detect() -> ServerEnvironment
}使用イメージ(Backend側):
// Before (Vapor直接)
@main
struct App {
static func main() async throws {
var env = try Environment.detect()
let app = try await Application.make(env)
try await configure(app)
try await app.execute()
}
}
// After (抽象化後)
@main
struct App {
static func main() async throws {
let server = try await Server.application()
try await configure(server)
try await server.run()
}
}public protocol RouteRegistrar: Sendable {
associatedtype Group: RouteGroup
// Simple route registration
func get(_ path: String..., handler: @escaping @Sendable () async throws -> some Encodable & Sendable)
func post(_ path: String..., handler: @escaping @Sendable () async throws -> some Encodable & Sendable)
// Route grouping
func group(_ path: String...) -> Group
// APIContract mounting (既存)
func mount<G: APIContractGroup, H: APIGroupHandler>(
_ group: G.Type,
handler: H
) -> MountedGroup<G, H> where H.Group == G
}
public protocol RouteGroup: RouteRegistrar {}public protocol ServerMiddleware: Sendable {
func handle(
request: ServerRequest,
next: @escaping @Sendable (ServerRequest) async throws -> ServerResponse
) async throws -> ServerResponse
}
// Built-in middleware
public struct CORSMiddleware: ServerMiddleware {
public init(configuration: CORSConfiguration = .default())
}
public struct CORSConfiguration: Sendable {
public static func `default`() -> CORSConfiguration
public static func custom(
allowedOrigins: [String],
allowedMethods: [APIMethod],
allowedHeaders: [String]
) -> CORSConfiguration
}public protocol ServerRequest: Sendable {
var pathParameters: [String: String] { get }
var queryParameters: [String: String] { get }
var headers: [String: String] { get }
var body: Data? { get }
var url: URL { get }
// Authentication
func authenticatedUserId() -> String?
}
public protocol ServerResponse: Sendable {
var status: HTTPStatus { get }
var headers: [String: String] { get }
var body: Data { get }
}
public struct HTTPStatus: Sendable, Equatable {
public let code: Int
public let reasonPhrase: String
public static let ok = HTTPStatus(code: 200, reasonPhrase: "OK")
public static let created = HTTPStatus(code: 201, reasonPhrase: "Created")
public static let noContent = HTTPStatus(code: 204, reasonPhrase: "No Content")
public static let badRequest = HTTPStatus(code: 400, reasonPhrase: "Bad Request")
public static let unauthorized = HTTPStatus(code: 401, reasonPhrase: "Unauthorized")
public static let notFound = HTTPStatus(code: 404, reasonPhrase: "Not Found")
public static let internalServerError = HTTPStatus(code: 500, reasonPhrase: "Internal Server Error")
}// Before: Vapor Content conformance required
extension WorkoutActivity: @retroactive Content {}
// After: Pure Codable (no Vapor dependency)
// WorkoutActivity already conforms to Codable - no changes neededAPIServerが内部でVaporのContent変換を行うため、アプリ側でのContent準拠は不要になる。
public protocol ServerLogger: Sendable {
func trace(_ message: @autoclosure () -> String)
func debug(_ message: @autoclosure () -> String)
func info(_ message: @autoclosure () -> String)
func warning(_ message: @autoclosure () -> String)
func error(_ message: @autoclosure () -> String)
}swift-api-server/Sources/APIServer/
├── Core/
│ ├── ServerApplication.swift # ServerApplication protocol + Server factory
│ ├── ServerEnvironment.swift # ServerEnvironment enum
│ ├── ServerLogger.swift # ServerLogger protocol
│ ├── HTTPStatus.swift # HTTPStatus struct
│ └── HTTPHeaders.swift # HTTPHeaders type
├── Routing/
│ ├── RouteRegistrar.swift # RouteRegistrar protocol
│ ├── RouteGroup.swift # RouteGroup protocol
│ └── MountedGroup.swift # (existing) MountedGroup
├── Middleware/
│ ├── ServerMiddleware.swift # ServerMiddleware protocol
│ ├── CORSMiddleware.swift # CORSMiddleware implementation
│ ├── AuthMiddleware.swift # (existing) AuthMiddleware
│ └── ErrorMiddleware.swift # (existing, modify to use abstractions)
├── Request/
│ ├── ServerRequest.swift # ServerRequest protocol
│ └── ServerResponse.swift # ServerResponse protocol
├── Contract/
│ ├── Application+Mount.swift # (existing) mount functionality
│ └── Request+Decode.swift # (existing) decode functionality
└── Internal/
└── Vapor/
├── VaporServerApplication.swift # Vapor implementation
├── VaporRequest.swift # Vapor Request adapter
├── VaporResponse.swift # Vapor Response adapter
└── VaporMiddleware.swift # Vapor middleware adapter
// Before
import APIServer
import Vapor // ← 削除対象
func configure(_ app: Application) async throws {
app.middleware.use(CORSMiddleware(configuration: .default()))
app.middleware.use(APIContractErrorMiddleware())
// ...
}
// After
import APIServer // Vapor import不要
func configure(_ server: some ServerApplication) async throws {
server.middleware(CORSMiddleware())
server.middleware(APIContractErrorMiddleware())
// ...
}// Before
import APIServer
import Vapor // ← 削除対象
func routes(_ app: Application, ...) throws {
app.get("health") { _ in "OK" }
app.grouped("v1").get("status") { _ in ["status": "running"] }
let activitiesRoutes = app.mount(ActivitiesAPI.self, handler: handler)
activitiesRoutes.register(ActivitiesAPI.List.self) { ... }
}
// After
import APIServer // Vapor import不要
func routes(_ server: some ServerApplication, ...) throws {
server.routes.get("health") { "OK" }
server.routes.group("v1").get("status") { ["status": "running"] }
let activitiesRoutes = server.routes.mount(ActivitiesAPI.self, handler: handler)
activitiesRoutes.register(ActivitiesAPI.List.self) { ... }
}// Before
import Vapor
@main
struct App {
static func main() async throws {
var env = try Environment.detect()
LoggingSystem.bootstrap(from: &env)
let app = try await Application.make(env)
defer { Task { try? await app.asyncShutdown() } }
try await configure(app)
try await app.execute()
}
}
// After
import APIServer
@main
struct App {
static func main() async throws {
let server = try await Server.application()
try await configure(server)
try await server.run()
}
}ServerApplicationprotocol +Server.application()factoryRouteRegistrarprotocol + route mountingServerMiddlewareprotocol adaptationCORSMiddlewareabstraction
ServerLoggerabstractionHTTPStatus/HTTPHeaderstypesServerRequest/ServerResponseprotocols (for custom handlers)
- Content protocol removal
- Advanced routing features
- Testing utilities
- 新しい抽象化APIを追加(既存APIは維持)
- Backendを新APIに移行
- 古いAPIをdeprecated化
- 次のメジャーバージョンで古いAPI削除
// 移行期間中の互換性維持
@available(*, deprecated, renamed: "ServerApplication")
public typealias VaporApplication = Application- 各抽象化の動作テスト
- Vapor実装アダプターのテスト
- 完全なリクエスト/レスポンスサイクル
- ミドルウェアチェーン
- 認証フロー
- 旧APIと新APIの互換性テスト
- パフォーマンス比較
| Phase | タスク | 複雑度 |
|---|---|---|
| 1.1 | ServerApplication | 高 |
| 1.2 | RouteRegistrar | 中 |
| 1.3 | ServerMiddleware | 中 |
| 1.4 | ServerRequest/Response | 低 |
| 2.1 | Content除去 | 低 |
| 3.1 | ServerLogger | 低 |
この抽象化により以下が可能になる:
- Vapor以外のフレームワーク(Hummingbird等)への切り替え
- テスト用のモックサーバー実装
- サーバーレス環境(AWS Lambda等)への対応