Vapor 5ベータでVapor 4のコードはどこが動かなくなるか|同じアプリを両方でビルドして確かめた書き換え箇所

SwiftのWebフレームワークVapor 5のベータが、10月13日のbeta.4まで進んだ

Swiftでサーバーを書くためのWebフレームワーク「Vapor」の開発チームは、2026年9月15日にVapor 5の最初のベータを公開しました。Vapor 4は約6年半にわたって使われてきた版で、Vapor 5はSwiftの async/await と構造化された並行処理を前提に作り直されています。GitHubのリリース一覧では、10月13日に5.0.0-beta.4が出ており、Vapor 4も10月21日に4.122.3が出ています。

iOSアプリをSwiftで書き、サーバー側もVapor 4で書いている人にとって気になるのは、今のコードをVapor 5へ移すときにどこを書き換えるかです。移行ガイドと詳しいドキュメントは、9月15日の公式ブログで「今後数週間のうちに出す」とされていた段階で、正式版の日程も決まっていません。

この記事では、同じ小さなアプリをVapor 4.122.3と5.0.0-beta.4で作り、Vapor 4のコードをそのままVapor 5でビルドしたときに出たエラーと、書き換えて動かした結果をまとめます。

この記事の要点

今回の小さなアプリでは、ルートの書き方の多くはそのまま使え、書き換えが必要だったのは起動部分、サーバー設定、EventLoopFuture、bodyのデコードと、Package.swiftの指定だった

Package.swiftに必要な指定

Swift 6.4とmacOS 26.2以上

beta.4のPackage.swiftはmacOS 26.2以上を求め、26.0の指定ではビルドが止まった。

Vapor 4のコード(ルート3つ)

4種類のコードのエラー

起動と終了、サーバーの設定、EventLoopFuture、bodyのデコードでビルドが止まった。

ルートの書き方

同じ応答を返した

app.get や req.parameters.require を使ったルートは、両方の版で同じ応答だった。

Vapor 5で増えた書き方

ストリーミングとマクロ

応答を少しずつ送る書き方と、@GETでルートを定義するマクロが動いた。

2026年10月26日、macOS 27(Apple Silicon)、Xcode 27.0のSwift 6.4で確認。

Vapor 5は非同期処理の土台とHTTPサーバーが入れ替わった

公式ブログに書かれた変更のうち、アプリのコードに直接関わるものを、Vapor 4と並べると次のようになります。

Vapor 4(4.122.3)

  • 今回のビルドはswift-tools 6.0、macOS 13の指定で通った
  • 非同期処理は EventLoopFuture と async/await の両方を使える
  • サーバーは SwiftNIO の上に作られ、設定は app.http.server
  • bodyは受け取ってからまとめて読む書き方が中心

Vapor 5(5.0.0-beta.4)

  • Swift 6.4以上。Package.swiftはmacOS 26.2以上を指定
  • EventLoop と EventLoopFuture は削除され、async/await だけになる
  • 新しい Swift HTTP Server の上で動き、設定は Swift Configuration で読む
  • request と response の body はストリーミングが基本

このほか、Application がサービスの寿命を管理する仕組み(Service Lifecycle)に組み込まれたこと、HTTPのステータスなどに HTTPTypes の型を使うこと、テスト用ライブラリの作り直し、使わない機能を外せる traits、実験的なマクロも挙げられています。beta.1の時点では、圧縮とWebSocketの対応はまだで、テンプレートからのLeafの利用も後日とされています。

Vapor 4のコードをVapor 5でビルドすると、4種類のコードのエラーで止まった

Vapor 4で動いている次のコードを、そのままVapor 5のプロジェクトに置いてビルドしました。ルートを3つ持つだけの小さなアプリで、1つは Vapor 4 の EventLoopFuture を返す書き方にしています。

// Vapor 4.122.3 で動くコード
import Vapor

struct Greeting: Content { var name: String }

@main
struct Entrypoint {
    static func main() async throws {
        var env = try Environment.detect()
        let app = try await Application.make(env)
        app.http.server.configuration.hostname = "127.0.0.1"
        app.http.server.configuration.port = 8084
        app.get("hello", ":name") { req -> String in
            let name = try req.parameters.require("name")
            return "Hello, \(name)"
        }
        app.post("greet") { req -> Greeting in
            let g = try req.content.decode(Greeting.self)
            return Greeting(name: g.name.uppercased())
        }
        app.get("future") { req -> EventLoopFuture<String> in
            req.eventLoop.makeSucceededFuture("from EventLoopFuture")
        }
        do { try await app.execute(); try await app.asyncShutdown() }
        catch { try? await app.asyncShutdown(); throw error }
    }
}

Swiftのコンパイラは、先に報告されたエラーを直すと、次のエラーを報告することがあります。エラーを直しながらビルドを繰り返し、4回目でビルドが通るまでに報告されたエラーを集めると次のとおりでした(行番号は省いています)。

# 1回目
error: missing argument for parameter 'from' in call        // Environment.detect()
error: type 'Application' has no member 'make'
error: cannot find type 'EventLoopFuture' in scope
# 2回目(起動部分を直し、EventLoopFuture のルートをいったん外したあと)
error: value of type 'Application' has no member 'http'
error: value of type 'Application' has no member 'execute'
error: value of type 'Application' has no member 'asyncShutdown'
# 3回目(サーバー設定と execute を直したあと)
error: expression is 'async' but is not marked with 'await'  // req.content.decode

コードとは別に、Package.swift の指定でもビルドが止まりました。ブログには「macOS 26以上」と書かれており、beta.4の Package.swift は macOS 26.2 を求めています。.macOS(.v26) と書いたプロジェクトでは、依存を解決したあとに次のエラーになりました(Linuxでは試していません)。

error: The package product 'Vapor-product' requires minimum platform version 26.2
for the macOS platform, but this target supports 26.0

起動部分、サーバー設定、EventLoopFuture、bodyの読み取りを書き換えると動いた

エラーになった箇所と、Vapor 5の開発用サンプル(リポジトリの Sources/Development)を参考に書き換えた内容をまとめます。2回目のビルドの前にいったん外した EventLoopFuture のルートは、最後に async の関数として書き直して足し、ビルドと応答を確かめました。

Vapor 4の書き方Vapor 5で通った書き方
Environment.detect() と Application.make(env)Application(configReader: ConfigReader(...))
app.http.server.configuration.hostname / portapp.serverConfiguration.address = .hostname(...)
app.execute() と app.asyncShutdown()app.run() と app.shutdown()
EventLoopFuture を返すルートasync の関数で値を返す
try req.content.decode(...)try await req.content.decode(...)

書き換えたあとのコードです。app.get と app.post でルートを書く部分、req.parameters.require、Content に準拠した型はそのまま使えました。ConfigReader は Swift Configuration の型で、Vapor 5 の依存として一緒に取り込まれ、import Configuration で使えました。

// Vapor 5.0.0-beta.4 でビルドできたコード
import Configuration
import Vapor

struct Greeting: Content { var name: String }

@main
struct Entrypoint {
    static func main() async throws {
        let app = try Application(configReader: ConfigReader(providers: [EnvironmentVariablesProvider()]))
        app.serverConfiguration.address = .hostname("127.0.0.1", port: 8086)
        app.get("hello", ":name") { req -> String in
            let name = try req.parameters.require("name")
            return "Hello, \(name)"
        }
        app.post("greet") { req -> Greeting in
            let g = try await req.content.decode(Greeting.self)
            return Greeting(name: g.name.uppercased())
        }
        // EventLoopFuture を返していたルートを async に書き換えたもの
        app.get("future") { req async throws -> String in
            "from async"
        }
        do { try await app.run(); try await app.shutdown() }
        catch { try? await app.shutdown(); throw error }
    }
}

書き換え前のVapor 4版と、書き換え後のVapor 5版のサーバーをそれぞれ起動し、同じリクエストを送りました。/hello と /greet の本文は同じで、表示したヘッダーでは名前の大文字・小文字の書き方が違いました。/future は、返す文字列を書き換えたぶんだけ違います。

===== Vapor 4.122.3(書き換え前、ポート8084)
$ curl -i http://127.0.0.1:8084/hello/Ken
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
Hello, Ken
$ curl -X POST http://127.0.0.1:8084/greet -H 'Content-Type: application/json' -d '{"name":"ken"}'
{"name":"KEN"}
$ curl http://127.0.0.1:8084/future
from EventLoopFuture

===== Vapor 5.0.0-beta.4(書き換え後、ポート8086)
$ curl -i http://127.0.0.1:8086/hello/Ken
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Hello, Ken
$ curl -X POST http://127.0.0.1:8086/greet -H 'Content-Type: application/json' -d '{"name":"ken"}'
{"name":"KEN"}
$ curl http://127.0.0.1:8086/future
from async

/greet は JSON を受け取るルートなので、Content-Type の指定が必要です。指定せずに送ると、どちらの版も 400 を返しました。

Vapor 5では、応答を少しずつ送る書き方とマクロのルートが使える

Vapor 5で新しく書けるようになった2つも試しました。1つ目はストリーミングの応答です。Response の body に書き込み用の関数を渡すと、書いた分から順にクライアントへ送られます。0.3秒ずつ待って3回書き込むルートを別に用意したVapor 5のアプリに足し、curl -N でアクセスして、1行目が届いてから各行が届くまでの秒数を記録しました。

app.get("stream") { _ -> Response in
    Response(body: .init(stream: { writer in
        for i in 1...3 {
            try await writer.write("chunk \(i)\n")
            try await Task.sleep(for: .milliseconds(300))
        }
    }))
}

$ curl -N http://127.0.0.1:8085/stream    # 左は1行目からの経過秒
0.00 chunk 1
0.33 chunk 2
0.64 chunk 3

3行は約0.3秒おきに届いていて、サーバーが書き込んだ分から順にクライアントへ送られていることが分かります。大きなファイルや、生成AIの出力のように少しずつできあがる応答を返すときに使える書き方です。

2つ目は、実験的なマクロでルートを定義する書き方です。@Controller を付けた型の中で、メソッドに @GET("todos", Int.self) のように書くと、パスの一部が Int の引数として渡されます。試すには、Package.swift のターゲットの依存に .product(name: "VaporMacros", package: "vapor") を足します。

import VaporMacros

@Controller
struct TodoController {
    @GET("todos", Int.self)
    func todo(req: Request, id: Int) async throws -> String {
        "todo #\(id)"
    }
}
// 起動時に登録: try await app.register(collection: TodoController())

$ curl http://127.0.0.1:8085/todos/42
todo #42
$ curl -i http://127.0.0.1:8085/todos/abc
HTTP/1.1 422 Unprocessable Entity

文字列の abc を渡すと、メソッドが呼ばれる前に 422 が返りました。Vapor 4では req.parameters.require("id", as: Int.self) のように取り出して変換していた処理を、型の宣言で書けます。beta.4では、認証などのミドルウェアをマクロで付ける機能も加わりました。

今のプロジェクトは、作業用のブランチで起動部分から書き換えを試す

公式ブログは、ベータは比較的安定しているものの、重要なサービスの本番ではまだ使わないよう勧めています。Vapor 4は引き続き不具合とセキュリティの修正を受けるので、正式版を待ちながら、Gitの作業用ブランチで次の順に影響を見積もっておけます。今回の小さなアプリで出たエラーは上の4種類でしたが、ミドルウェア、テスト、データベースを使うプロジェクトでは、ほかの箇所でも書き換えが出ます。

1. ビルド環境をそろえる

Swift 6.4のツールチェーン(今回はXcode 27.0)を用意し、Package.swift を swift-tools-version 6.4、platforms を .macOS("26.2") にする。Swift 5の言語モードで書いているプロジェクトは、先にVapor 4のままSwift 6のモードへ移しておくと、並行処理の検査で出るエラーとVapor 5の変更によるエラーを分けて直せる。

2. 起動とサーバー設定を書き換える

Application.make と app.http.server を、Application(configReader:) と app.serverConfiguration に置き換える。

3. EventLoopFuture を数える

grep -rn "EventLoopFuture\|eventLoop\." Sources で使っている箇所を数え、async/await への書き換え量を見積もる。.map や .flatMap でつないだ処理、EventLoopPromise、.wait() も書き換えの対象になるので、検索の件数は目安として使う。

4. コンパイラの指摘に従って await を足す

req.content.decode のように async になった呼び出しは、ビルドエラーの位置に await を足して直す。

Fluent(データベース)、JWT、Queues などの関連パッケージのVapor 5対応は作業中とされています。Fluentは、Vapor 5用のテンプレートでFluentKitを直接使う形で試せます。これらを使うプロジェクトは、上の手順で本体の書き換え量を確かめておき、関連パッケージの対応版が出てから移行の時期を決めると、作業を2回に分けずに済みます。