What’s New, Upgrading and Vapor 5

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page was written on 2026-10-17 against Vapor 4.122.2 (tagged 2026-09-17, the latest 4.x release) on Swift 6.4, and against Vapor 5.0.0-beta.4 (tagged 2026-10-13), which is a beta and not generally available. Both were compiled and run in scratch packages (Vapor 4.122.2 with swift-tools-version:6.3 in the Swift 6 language mode, Vapor 5.0.0-beta.4 with tools 6.4 on macOS 27), and the curl output and compiler diagnostics shown are from those runs. Every Vapor 5 statement below was read from the 5.0.0-beta.4 tag, its release notes and its Package.swift, because the official Vapor 5 documentation at https://docs.vapor.codes/5.0/ was still a single page that says more documentation is coming. Treat everything about Vapor 5 as subject to change before 5.0.0.

The page has three jobs:

  1. Show how the framework changed, so that you can read and upgrade code, blog posts and books written for Vapor 2, 3 and early 4.

  2. Describe Vapor 5 as it is in the beta, and give advice that keeps a Vapor 4 code base cheap to move.

  3. Collect, in one place, every statement of the three consulted books that is outdated or unsafe, and every place where the current official documentation or a library differed from what was found when this section’s pages were verified.

The other pages of this section already say, in their own closing section, where the books are outdated for their topic. This page is the index: the tables below are organised by area and by book chapter, and link to the page that has the current form.

Where Vapor stands

Line Tags Toolchain of its era Status on 2026-10-17

Vapor 2

2.0.0 (2017-05-17)

Swift 3 and 4, Xcode 8 and 9

Unmaintained. Documented at the archived 2.0 version of the docs site. The Patel book is written for it.

Vapor 3

3.0.0 (2018-05-04)

Swift 4.1 and 4.2, Xcode 9 and 10

Unmaintained. Documented at the archived 3.0 version of the docs site. The Yeung book and the later chapters of the Kodeco book are written for it.

Vapor 4

4.0.0 (2020-04-09) to 4.122.2 (2026-09-17)

Swift 5.2, Xcode 11.4 and macOS 10.15 for the first releases

Current. 4.122.2 needs Swift 6.0 or newer (swift-tools-version:6.0), builds without warnings in the Swift 6 language mode and runs on macOS and Linux. Vapor 4 is in maintenance mode; the Vapor blog promises bug and security fixes for at least six months after Vapor 5 is released, and a support period of at least three years for Vapor 5.

Vapor 5

No stable tag. 5.0.0-alpha.1 (2026-06-11), alpha.2 (2026-08-11), beta.1 (2026-09-15), beta.2 (2026-09-16), beta.3 (2026-10-01), beta.4 (2026-10-13)

Swift 6.4 (swift-tools-version:6.4) and macOS, iOS, tvOS and watchOS 26.2 on Apple platforms

Beta, and a major rewrite. Not for production, and not yet supported by most of the ecosystem (see Ecosystem readiness).

The dates are those of the GitHub releases of vapor/vapor. The Vapor blog announced the plan for Vapor 5 with a first alpha in mind for the release of Swift 6 in 2024; the first alpha appeared on 2026-06-11, which is a reminder not to plan a project around a Vapor 5 date. Every example in this section targets Vapor 4.122.x.

Timeline of Vapor 2, Vapor 3, Vapor 4 and the Vapor 5 beta with the main change of each major version

Upgrading from Vapor 2 and Vapor 3 to Vapor 4

Vapor 4 is a rewrite, not a rename. The official guide Upgrading to 4.0 covers Vapor 3 to 4.0 only, and it is itself frozen at Swift 5.2 and Xcode 11.4: its main.swift example still uses the Application(env) initializer that 4.113 deprecated, it chains EventLoopFuture values with flatMapThrowing, and it predates async/await. Use it for the mapping of concepts, and apply the current forms from the tables below. The current documentation has no Vapor 2 to 3 guide; the Vapor 2 entries below describe what the Patel book teaches.

Upgrade in the order of the tables: the project and the process first (nothing else compiles until they are right), then services and configuration, routing and requests, concurrency, Fluent, Leaf and authentication, and the tests last.

The project and the process

Area Vapor 2 and 3 (the books) Vapor 4.122.2 (this section)

Targets and layout

V3: Sources/App/{app,boot,configure,routes}.swift plus a Sources/Run/main.swift target, swift-tools-version:4.0. V2: a Droplet configured from Config/*.json.

One executable target App with entrypoint.swift, configure.swift and routes.swift, swift-tools-version:6.3 in the template (6.0 is the minimum), automatic test discovery, and DTOs/, Models/ and Controllers/ folders. See Project structure and SwiftPM.

Entrypoint

V3: app(env).run(). V2: Droplet() then drop.run(). Early Vapor 4 and the official upgrade guide: Application(env), defer { app.shutdown() }, try app.run().

@main enum Entrypoint with Environment.detect(), LoggingSystem.bootstrap(from:), Application.make(env), configure(app), app.execute() and app.asyncShutdown(). See Application lifecycle and entrypoint.

Toolchain

Swift 4 or 5.x, Xcode 9 to 11, Ubuntu 14.04 to 18.04, the Vapor APT repository, check.vapor.sh, vapor build, vapor run, vapor xcode, vapor cloud, vapor heroku and swift package generate-xcodeproj.

Swift 6.x through Swiftly, Xcode or a Docker image. Toolbox 20 only creates projects (vapor new); open Package.swift directly in Xcode or VS Code. See Installation and toolchain.

Dependencies

V3: vapor/auth, vapor/core, vapor/crypto, vapor/multipart, vapor/url-encoded-form, and database packages such as fluent-postgresql.

Authentication, SwiftCrypto, multipart and URL-encoded forms are part of vapor; fluent is a separate dependency and every database package is named fluent-<database>-driver. See Fluent overview and drivers.

The entrypoint that replaces all of the first three rows. It is the template’s entrypoint.swift without the commented-out executor takeover, and configure and routes are the two functions of the next listings:

import Logging
import Vapor

@main
enum Entrypoint {
    static func main() async throws {
        var env = try Environment.detect()
        try LoggingSystem.bootstrap(from: &env)

        let app = try await Application.make(env)

        do {
            try await configure(app)
            try await app.execute()
        } catch {
            app.logger.report(error: error)
            try? await app.asyncShutdown()
            throw error
        }
        try await app.asyncShutdown()
    }
}

The forms that the books and early Vapor 4 projects use still compile in 4.122.2, but they are deprecated. A scratch target containing the official upgrade guide’s main.swift:

import Vapor

var env = try Environment.detect()
let app = Application(env)
defer { app.shutdown() }
try app.run()

Built with swift build, that target produced this diagnostic and no other:

Sources/Legacy/main.swift:5:11: warning: 'init(_:_:)' is deprecated: Migrate to using the async APIs. Use use Application.make(_:_:) instead

The synchronous shutdown() is marked noasync since 4.99.0, so it is flagged inside an async function but not in top-level code like the listing. Vapor 3’s app(env) function, the Run target and main.swift have no counterpart that compiles.

Services, configuration and providers

Area Vapor 2 and 3 (the books) Vapor 4.122.2 (this section)

Configuration function

V3: configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) and a separate boot.swift. V2: JSON files under Config/, with per-environment subfolders and $PORT:8080 style environment references.

func configure(_ app: Application) async throws; boot.swift content moves into it. Settings are properties of app. See Configuration and environment.

Services and providers

V3: services.register(...), config.prefer(...), try req.make(Client.self), Container, Service, ServiceType. V2: Providers added to the Droplet.

Services are properties and methods of Application and Request (req.client, req.db, req.view), added with extensions. Stored properties use app.storage and a StorageKey; startup and shutdown hooks use LifecycleHandler. See Services, storage and dependency injection.

Server settings

V3: services.register(NIOServerConfig.default(port: 8281)).

app.http.server.configuration.port = 8281. See Server configuration.

Middleware

V3: MiddlewareConfig, middlewares.use(ErrorMiddleware.self) by type, services.register(middlewares).

app.middleware.use(...) with an instance (ErrorMiddleware.default(environment: app.environment)), at .beginning or .end, and grouped(...) on routes. See Middleware.

Environment

V3: Environment.get. V2: Config/secrets and $VAR:default.

Environment.get, Environment.process, .env and .env.<environment> files, Environment.secret(key:), app.environment. See Configuration and environment.

Commands and console

V3: CommandConfig registered in configure and the separate Console package.

ConsoleKit commands with Signature, registered on app.asyncCommands; AsyncCommand is the current protocol. See Commands.

Logging

V3: LoggerAPI (IBM) or Vapor’s own Logger service.

swift-log, bootstrapped with LoggingSystem.bootstrap(from:), req.logger and app.logger. See Logging.

Routing, requests and content

Area Vapor 2 and 3 (the books) Vapor 4.122.2 (this section)

Registering routes

V3: routes(_ router: Router, _ container: Container) and router.get("books", Int.parameter). V2: drop.get, drop.resource("books", BookController()) with ResourceRepresentable.

routes(_ app: Application) and app.get("books", ":bookID"). Resource controllers are RouteCollection values. Paths are comma separated and never contain a slash inside a component. See Routing and Controllers and route collections.

Path parameters

V3: try req.parameters.next(Book.self); the Parameter protocol, and models as path components.

req.parameters.get("bookID", as: UUID.self) or require; look the model up with Book.find(_:on:).

Request bodies

V2: request.json, Node, JSONConvertible, makeJSON(). V3: req.content.decode(User.self) returned a Future and req.http wrapped HTTPRequest.

let user = try req.content.decode(User.self) is synchronous and throwing in 4.x, because the body is collected first; req.http is gone. Large bodies use body: .stream. See Content and Codable and Request and response.

Validation

V3: a different Validatable with validations() returning an array.

static func validations(_ validations: inout Validations) and req.content.decode followed by validate(content:). See Validation.

Errors

V2 and V3: Abort plus Debuggable in a separate package.

Abort, AbortError and DebuggableError in Vapor, and ErrorMiddleware. See Error handling.

Files and uploads

V3: DirectoryConfig, req.streamFile, FileMiddleware registered through MiddlewareConfig.

app.directory, req.fileio, FileMiddleware(publicDirectory:) added with app.middleware.use, File in a Content struct. See Static files and file I/O and File uploads and streaming.

Sessions and caching

V3: KeyedCache, MemoryKeyedCache, SessionsMiddleware registered through services; a Redis provider with jsonSet and jsonGet.

app.sessions, app.caches.use(.memory), .fluent or .redis, and req.cache. See Sessions and Redis and caching.

HTTP client

V3: try req.make(Client.self).get(url).

req.client.get(url). See HTTP client.

Concurrency: from futures to async/await

Vapor 3 had its own Future<T> type with map(to:) labels and global flatMap helpers. Vapor 4.0 dropped it and exposed SwiftNIO’s EventLoopFuture directly (map, flatMap, flatMapThrowing, and, makePromise(of:)). Vapor 4.50.0 (2021-10-26) added async/await APIs, and since then the recommended handler is an async throws function that returns its value.

Era What to expect in code and in books

Vapor 3

Future<Response>, req.content.decode(User.self).flatMap { ... }, req.parameters.next(...), wait() in tests.

Vapor 4.0 to 4.49

EventLoopFuture<Response> return types; synchronous req.content.decode; app.autoMigrate().wait() in configure.

Vapor 4.50 and later

async throws handlers, AsyncMiddleware, AsyncMigration, AsyncCommand, AsyncJob, AsyncBasicAuthenticator; futures bridged with .get().

Vapor 4.99 to 4.122

Sendable everywhere: Application and Request are Sendable, route closures are @Sendable, storage values must be Sendable, models are final class ...: Model, @unchecked Sendable.

Vapor 5 beta

No EventLoopFuture API at all; structured concurrency throughout. See Vapor 5 beta.4.

The legacy operators, the bridges and the Swift 6 rules are on Swift concurrency in Vapor. The future-returning handler of a book still compiles in 4.122.2, which is why a half-upgraded project can build and still be wrong about Sendable.

Fluent, migrations, Leaf and authentication

Area Vapor 2 and 3 (the books) Vapor 4.122.2 (this section)

Models

V2: Model with Node, Entity, Row and Preparation. V3: PostgreSQLModel, SQLiteModel or MySQLModel structs with var id: Int?, static let entity and Migration conformance on the model; the Parameter protocol.

final class Book: Model, @unchecked Sendable with static let schema, @ID(key: .id) (the template now writes @ID), @Field(key:), @Timestamp(key:on:), an empty init(), and key paths like \.$title. See Fluent models.

Relations

V3: computed parent(\.authorID), children(\.authorID), siblings().

@Parent, @Children(for:), @Siblings(through:from:to:), @OptionalChild, pivots as ordinary models, with(\.$author) for eager loading. See Fluent relations.

Migrations

V3: extension Todo: Migration {} with reflection-based auto migration, static func prepare(on conn:), Database.create(Todo.self, on: conn) and MigrationConfig.

A separate AsyncMigration value with prepare(on:) and revert(on:), database.schema("books")...create(), registered with app.migrations.add and run by the migrate command or try await app.autoMigrate(). See Migrations and schema.

Database configuration

V3: DatabasesConfig, FluentPostgreSQLProvider, services.register.

app.databases.use(.postgres(configuration: SQLPostgresConfiguration(...)), as: .psql) with explicit TLS. There is no provider registration step.

Queries and raw SQL

V3: Planet.query(on: req), filter(\.foo == x), withPooledConnection, deletedAtKey, alsoDecode.

Planet.query(on: req.db), filter(\.$foo == x), @Timestamp(on: .delete) soft delete, paginate, SQLKit for raw SQL with bound interpolation. See Fluent queries and pagination and SQLKit and raw SQL.

Leaf

Leaf 2 and 3: #embed, #set and #get, #loop, brace blocks, LeafProvider, req.make(LeafRenderer.self).

Leaf 4: #extend, #export and #import, #if(...): with #endif, #for(...): with #endfor, app.views.use(.leaf), req.view.render. See Leaf templates.

Authentication

V2: auth-provider. V3: vapor/auth with PasswordAuthenticatable, TokenAuthenticatable, BCryptDigest, AuthenticationSessionsMiddleware, PersistMiddleware.

Built in: Authenticatable, AsyncBasicAuthenticator, AsyncBearerAuthenticator, ModelAuthenticatable, ModelTokenAuthenticatable, ModelSessionAuthenticatable, req.auth, app.passwords, Bcrypt. JWT 5 (app.jwt.keys, req.jwt.verify) is covered by none of the books. See Authentication and JWT.

OAuth and email

Imperial 0.7 (Vapor 3); the Vapor 3 SendGrid provider.

Imperial 2.x; sendgrid-kit, Mailgun 6 or an SMTP package; Sign in with Apple through req.jwt.apple. See OAuth and Sign in with Apple.

Repositories deserve a note, because the pattern is in the official upgrade guide and in the books. In Vapor 3 a repository was a final class registered as a ServiceType. In Vapor 4 it is a protocol, a struct that takes the database, a factory stored in app.storage and an accessor on Request; with async/await the protocol methods are async throws. Services, Storage and Dependency Injection builds it step by step.

Testing

Era Tool Shape

Vapor 2 and 3

XCTest with Droplet.testable() or a hand-built Request(http:using:) and wait()

Futures and explicit event-loop handling.

Vapor 4.0 to 4.109

XCTVapor, app.test(.GET, "path") { res in ... }

Synchronous test method; XCTAssertEqual.

Vapor 4.110 and later

VaporTesting with Swift Testing (@Test, #expect, withApp, app.testing().test(...))

async all the way; XCTVapor is kept as the legacy module.

The test below compiles and passes against 4.122.2. It is the whole test of the books route used in this page’s scratch package:

import Testing
import VaporTesting

@testable import App

@Suite struct AppTests {
    @Test func listBooks() async throws {
        try await withApp(configure: configure) { app in
            try await app.testing().test(.GET, "books") { res async throws in
                #expect(res.status == .ok)
                let books = try res.content.decode([BookDTO].self)
                #expect(books.first?.title == "Swift for Servers")
            }
        }
    }
}

XCTVapor still works, with one change from the books' examples. Calling the synchronous app.test(...) from an async test method is a compile error in 4.122.2 (instance method 'test' is unavailable from asynchronous contexts; Use the async method instead), so the XCTest form is try await app.test(.GET, "books/7") { res async in ... }. Vapor 5’s VaporTesting has new APIs again (see below). Testing covers the details.

The 4.x handlers and the controller the tests exercise are below. The list and show handlers are the current form; legacyList is the EventLoopFuture shape of the books, which still compiles in 4.122.2 and is shown only so that you can recognise it.

import Vapor

struct BookDTO: Content {
    var id: Int
    var title: String
}

struct BookController: RouteCollection {
    func boot(routes: any RoutesBuilder) throws {
        let books = routes.grouped("books")
        books.get(use: list)
        books.get(":bookID", use: show)
        books.get("legacy", use: legacyList)
    }

    @Sendable
    func list(req: Request) async throws -> [BookDTO] {
        [BookDTO(id: 1, title: "Swift for Servers")]
    }

    @Sendable
    func show(req: Request) async throws -> BookDTO {
        let id = try req.parameters.require("bookID", as: Int.self)
        return BookDTO(id: id, title: "Book \(id)")
    }

    // Legacy shape: still compiles in 4.122.2, returns an EventLoopFuture.
    @Sendable
    func legacyList(req: Request) -> EventLoopFuture<[BookDTO]> {
        req.eventLoop.makeSucceededFuture([BookDTO(id: 1, title: "Swift for Servers")])
    }
}

func routes(_ app: Application) throws {
    app.get("hello") { _ async in "Hello, Vapor 4" }
    try app.register(collection: BookController())
}

Running it (swift run App serve -p 8088) and calling it with curl:

$ curl -s localhost:8088/hello
Hello, Vapor 4
$ curl -s localhost:8088/books/legacy
[{"id":1,"title":"Swift for Servers"}]

Notable 4.x releases

The table lists the releases that changed how an application is written, with the dates and the change as stated in the GitHub release notes. The full history is in the release list; exact release numbers for several earlier changes (for example the first Sendable conformances) are in the blog post Upcoming Sendable changes rather than in one release.

Release Date Change for application code

4.0.0

2020-04-09

The rewrite: services on Application and Request, SwiftNIO types exposed directly, Fluent 4 with property wrappers.

4.50.0

2021-10-26

async/await APIs for Client, routes, ResponseEncodable, Middleware (AsyncMiddleware), password hashing, Cache and views.

4.89.0

2023-12-06

Fully async entrypoints: use execute() instead of run() and startup() instead of start().

4.99.0

2024-05-10

asyncShutdown(); the synchronous shutdown() becomes noasync.

4.100.0

2024-05-15

Async LifecycleHandler methods, needed by packages such as Redis whose shutdown used to call wait().

4.110.0

2024-12-21

The VaporTesting module for Swift Testing, with app.testing().

4.113.0

2025-02-10

Application.init deprecated in favour of the async Application.make.

4.116.0

2025-09-09

Application.make accepts a Logger.

4.121.0

2026-01-05

RouteNotFound, the error thrown when no route matches, is public, so a middleware can handle 404s for every method.

4.122.0

2026-07-01

idleTimeout on HTTPServer.Configuration (not in the official documentation yet).

4.122.1

2026-08-25

Fix for multiple response-stream callbacks.

4.122.2

2026-09-17

Sends Accept-Ranges instead of Accept on file responses.

Swift 6 adoption in Vapor 4 happened in steps rather than in one release: a large Sendable annotation effort in 2023 (using locks inside Application and Request), async streaming bodies for requests and responses, and by 2026 a package that builds with strict concurrency and no warnings in the Swift 6 language mode. The template of the 4.121 line enables the upcoming features ExistentialAny, InternalImportsByDefault, MemberImportVisibility, InferIsolatedConformances and ImmutableWeakCaptures. Two practical consequences for code copied from older material: route closures and handler methods should be @Sendable when the enclosing type could gain non-Sendable state, and a value stored under a StorageKey must be Sendable.

4.122.2 behaviour that differs from the documentation

Verifying this section against the source of 4.122.2 and the libraries it resolves turned up statements in the official documentation that no longer match the code, and a few defects. They are collected here with the page that proves each one. A row is a statement about the checked version, not a documented guarantee: re-test when you upgrade.

Topic What the documentation (or an older article) says What 4.122.2 and its libraries do Page

Request decompression

The server configuration page says the default is .disabled with .ratio(10).

The initializer in the source defaults to .enabled with .ratio(25).

Server configuration

Idle timeout

Not documented.

HTTPServer.Configuration.idleTimeout exists since 4.122.0 (pull request 3473).

Server configuration

Session cookie name

The sessions page says the default cookie is vapor_session.

The source sets vapor-session with a hyphen. Do not hard-code either spelling in a client.

Sessions

boot command

Older documentation lists a boot command that only boots the application.

BootCommand exists, but Application.Core.Storage.init registers it on a local command set and then replaces that set with an empty one, so swift run App boot fails with .unknownCommand("boot", ...). Use routes or a command of your own as a CI boot check.

Application lifecycle and entrypoint

Shutdown order

Not documented.

Lifecycle handlers shut down before the HTTP server unless in-flight work is drained first.

Application lifecycle and entrypoint

Environment and entrypoint

The environment page shows Application(env) with defer { app.shutdown() }.

That initializer is deprecated; VAPOR_ENV, Environment.secret and the quote and \n rules of the .env parser are documented only in the source.

Configuration and environment

Installation and project docs

The installation page quotes Swift 5.9 as the minimum; the folder-structure page shows XCTest and XCTVapor.

Vapor 4.122 needs Swift 6.0; the template tests use Swift Testing with VaporTesting.

Installation and toolchain and Project structure and SwiftPM

Validatable doc comment

The comment on the protocol shows validations() -> [Validation].

The real requirement is static func validations(_ validations: inout Validations).

Validation

Passwords

The page does not say what happens with a malformed digest.

verify throws for an unknown or malformed digest instead of returning false.

Passwords and crypto

JWT

The page shows swift-tools-version:5.10, from: "5.0.0", EdDSA.PrivateKey(curve: .ed25519) without try, and says a key without a kid becomes the default.

In jwt-kit 5.7.1 the EdDSA initializer throws, and the first key added is also the default when it has a kid; an unknown kid falls back to it.

JWT

Leaf

The page says from: "4.4.0" and that the cache is off during development.

Leaf 4.5.2 is the current release, and the cache is disabled for the .development environment in its source.

Leaf templates

WebSockets

The page shows the EventLoopFuture forms of shouldUpgrade and onUpgrade and synchronous send.

Both still work in 4.122; the async overloads are the current form. The onPing overload without buffer is deprecated in websocket-kit 2.16.2.

WebSockets

Fluent chunk

chunk(max:) delivers the rows in batches.

The async overload in fluent-kit 1.57.0 never delivers the final, smaller batch: 26 rows arrive as 10 and 10, and the last 6 are dropped. The same file on main (2026-10-17) has the same structure. The future overload awaited with .get() delivers 10, 10 and 6.

Fluent queries and pagination

Raw SQL fragments

Older articles and the Vapor docs use .sql(raw:) in schema and queries and appendInterpolation(raw:) in SQLKit.

.sql(raw:) is deprecated since fluent-kit 1.57.0 in favour of .sql(unsafeRaw:), and SQLKit 3.36.0 renames the interpolation to unsafeRaw:. Both compile with warnings.

Migrations and schema and SQLKit and raw SQL

MongoDB

The vendor driver mongodb-vapor is an option next to the community one.

MongoDB’s Swift driver and mongodb-vapor were archived in 2023; the community fluent-mongo-driver 1.4.0 (2024-10-06) is the latest release.

MongoDB

Queues

Not documented.

queues 1.18.0 documents QueuesCommand.init(application:scheduled:) as a historical artifact whose scheduled argument does nothing; the flag is --scheduled. A monthly schedule on .last reaches a fatalError (Last day of the month is not yet supported.) that crashes the process at start; a malformed time literal such as "8am" also stops it, and an incomplete builder (daily() without at) never schedules and logs only at debug level. The Redis driver of 1.1.2 leaves a job id in its processing list after a crash and never re-runs it, and delayed jobs at the head of the queue hold back ready jobs. The Fluent driver m-barthelemy/vapor-queues-fluent-driver is archived; use the vapor-community fork (3.2.0).

Queues and scheduled jobs

APNs

The Vapor page shows vapor/apns from: "4.0.0" and synchronous calls.

vapor/apns 5.1.0 (2026-10-08) depends on APNSwift 7.0.1 or later; APNSContainers is an actor and the client accessors are async; APNSEnvironment.sandbox is deprecated for .development. APNSwift 7.1.0 lives under github.com/kylebrowning/APNSwift (the old organisation URL redirects), and 8.0.0-beta1 (2026-10-16) is a pre-release: pin 7.1.0.

APNs push notifications

OpenAPI

The swift-openapi-vapor pages describe the generated server as an ordinary Vapor route set.

swift-openapi-runtime 1.13.1 collects JSON bodies without a limit and VaporTransport registers routes with body: .stream, so Vapor’s maxBodySize does not apply; registerHandlers defaults serverURL to / (pass Servers.Server1.url()); format: uuid generates String; a typed Location header containing / is percent-encoded; the security section of the document is ignored. swift-openapi-vapor 1.2.0 needs Vapor 4 (from 4.106.7); 2.0.0-beta.1 needs Vapor 5.0.0-beta.4, tools 6.4 and the 26.2 platforms.

OpenAPI

Email packages

Not documented by Vapor.

The Smtp package of Sending Email (3.1.2) is not under vapor-community, was last released in 2024-09 and is not Sendable-clean; sendgrid-kit is 3.1.0; Mailgun 6.1.0 needs tools 6.2.

Sending Email

The site’s apps/apple/networking.adoc

RemoteNote.notebookID with keyDecodingStrategy = .convertFromSnakeCase decodes notebook_id.

It never does: notebook_id becomes notebookId, so the optional stays nil silently. A CodingKeys enum with case notebookID = "notebookId" fixes it. The Apple networking page of this site had this defect and now carries that fix (see Networking).

Full-Stack Swift with iOS Clients

Vapor 5 beta.4

Vapor 5 is a major overhaul of Vapor’s internals. The summary below is what the 5.0.0-beta.4 tag contains; it was compiled and run, but the APIs are explicitly not frozen. The release notes from the alphas to beta.4 are the best change log there is.

What each beta added

Release Date Highlights from the GitHub release notes

5.0.0-alpha.1

2026-06-11

First alpha, announced at WWDC. Built on a new HTTP server from Swift, removal of seven years of technical debt, full structured concurrency, type-safe routing with macros for parameters and authentication, and a controller macro.

5.0.0-alpha.2

2026-08-11

HTTP server pinned and updated, TLS wired to the server, Route becomes a struct, "Modernise Authentication" (async-only authenticators, a challenge header on basic auth), task-local logger, MultipartKit beta.

5.0.0-beta.1

2026-09-15

HTTP/2 on the server, request and response bodies streamed through the server handler, Response and Request overhauls, NIO removed from the public API of Response and ByteBuffer from the content coders, file operations removed, Application overhaul, new View type, new SocketAddress, mTLS, TLS certificate reloading, router methods for the QUERY HTTP method, new VaporTesting APIs, request body streaming by default.

5.0.0-beta.2

2026-09-16

Fixes SwiftPM refusing to resolve the dependency graph (uses a tagged HTTP server).

5.0.0-beta.3

2026-10-01

OTP removed, support for Crypto 5, DotEnv removed, application factories for the file middleware, an HTTP server update, Swift Format, initial benchmarks.

5.0.0-beta.4

2026-10-13

Middleware macros.

The package

The manifest of 5.0.0-beta.4 uses swift-tools-version:6.4 and the 26.2 platform minimum for macOS, iOS, tvOS and watchOS, so it cannot be built on an older Xcode or on an Apple platform below that (the Linux requirement is the Swift 6.4 toolchain). It defines SwiftPM traits, all four enabled by default, and three library products (Vapor, VaporTesting and VaporMacros).

Trait What it controls

WebSockets

WebSocket support. In the beta the websocket-kit dependency and its product are commented out with the note "Disable until websockets are working", so the trait exists but WebSockets are not usable yet.

HTTPClient

The AsyncHTTPClient-based Client. Without it a BlackholeClient is the default client.

Multipart

Multipart support, on multipart-kit 5.0.0-beta.

MacroRouting

The @Controller, @GET, #Middleware and related macros in the VaporMacros product.

The main dependencies are swift-http-server (.upToNextMinor(from: "0.3.0")), swift-configuration 1.x with the CommandLineArguments trait, swift-service-lifecycle from 2.6.3, swift-http-types from 1.7.0, swift-crypto from 4.0.0 up to 6.0.0, swift-nio from 2.101.3, swift-nio-ssl, swift-nio-extras, swift-certificates, swift-syntax 602, the tracing, metrics and logging APIs, console-kit 5.0.0-beta and routing-kit 5.0.0-beta.

What changes for application code

Everything in the "Vapor 5 beta.4" column was read from the sources of the tag. "Removed" means that the file or the public API is not in the tree of 5.0.0-beta.4; it may come back in another form.

Area Vapor 4.122.2 Vapor 5.0.0-beta.4

Server

Vapor’s own NIO pipeline, app.http.server.configuration (HTTPServer.Configuration), serve flags -H, -p, -b.

swift-http-server; app.serverConfiguration is a ServerConfiguration (address, tlsConfiguration, httpVersions that defaults to HTTP/1.1 only, eTagHashCacheCapacity, maxDrainBytes of 16 KB). HTTP/2 needs TLS. mTLS and certificate reloading are in. The configuration is frozen once the application starts.

Application

Application.make(env) (async), execute(), asyncShutdown(); Application is a Sendable class holding its services in app.storage.

try Application(configReader:) is a throwing synchronous initializer on a final class Application: Sendable, Service. run() runs it as a service without signal handling, so that you can place it in your own ServiceGroup; start() runs it with SIGTERM and SIGINT handlers; shutdown() is async throws and not calling it before deinit trips an assertion. There is a state property.

Services

Extensions on Application, app.storage with StorageKey, app.locks, app.lifecycle.use(...), app.clients.use, app.caches.use, app.views.use, app.sessions.use.

The view renderer, cache, responder, server, client and session driver are chosen once, through Application.ServiceConfiguration passed to init. Extra services are added with addService(_:) (any ServiceLifecycle.Service) and addLifecycleHandler(_:). StorageKey, app.storage and app.locks are not in the tree.

Configuration

Environment.get, .env files (DotEnv), Environment.secret, --env and VAPOR_ENV.

swift-configuration: Environment.detect(from:) reads the vapor.env key of a ConfigReader whose default providers are the command-line arguments and the process environment. The address keys are hostname, port, bind ("hostname:port") and unix-socket. DotEnv, Environment+Process and Environment+Secret are gone. The beta does not yet document what replaces .env files and secrets.

Request and response

Request and Response are classes with NIO types (HTTPMethod, HTTPHeaders, ByteBuffer) in their public API.

Request, Response and Route are Sendable structs. Method, status and headers use swift-http-types (HTTPRequest.Method, HTTPResponse.Status, HTTPFields). ResponseEncodable is a single async throws protocol. NIO types are not in the public Response API.

Bodies

Collected by default (.collect(maxSize:), 16 KB), .stream per route.

Streaming by default: try await req.body.forEachChunk { ... }, a single-consumer body (two tasks reading it at once throw RequestBodyAlreadyBeingRead, and collecting what is left after a partial read throws RequestBodyPartiallyConsumed), maxBodySize: per route, Response(body: .init(stream: { writer in ... })) with an awaiting writer that gives back-pressure.

Content

try req.content.decode(T.self), synchronous.

try await req.content.decode(T.self); validate(content:) becomes async throws.

Middleware

AsyncMiddleware.respond(to:chainingTo: any AsyncResponder).

Middleware: Sendable with respond(to:chainingTo: any Responder) async throws -> Response; the Async prefix is gone. The default stack is still RouteLoggingMiddleware then ErrorMiddleware. The Middlewares type keeps use(_:at:) with .beginning and .end.

Routing

app.get, grouped, RouteCollection, PathComponent.

The same, plus macro routing with the MacroRouting trait (below), and router methods for QUERY.

Authentication

AsyncBasicAuthenticator, AsyncBearerAuthenticator, AsyncCredentialsAuthenticator, ChallengeMiddleware.

Async-only protocols without the prefix; Authenticatable refines Sendable; basic and bearer authenticators add a WWW-Authenticate challenge on a 401. Guard and redirect middleware keep their names. Fluent’s Model*Authenticatable protocols are not part of vapor.

Passwords, OTP and crypto

app.passwords, req.password, Bcrypt, OTP.swift (HOTP, TOTP).

The Passwords, Bcrypt and Security directories and OTP are not in the tree; swift-crypto is still a dependency, and Base32, Base64 and secureCompare helpers remain. The beta does not show where password hashing moves: keep hashing behind a small protocol of your own.

Files

req.fileio with readFile, writeFile, collectFile, streamFile; FileMiddleware(publicDirectory:).

Removed in 5.0.0-beta.1 (pull request 3553): FileIO is reached through app.fileio and its only public method is streamFile(at:for:chunkSize:mediaType:advancedETagComparison:onCompleted:). Read and write files with Foundation, swift-system or NIOFileSystem. The file middleware is created with app.makeFileMiddleware(...).

Cache, sessions, views

Cache with futures and for(_:); SessionDriver with futures; ViewRenderer returning a future.

Cache is Sendable with two async throws requirements (get and set) and a MemoryCache default; the session driver is fixed at init (MemorySessions by default); ViewRenderer has one async throws requirement and the default is a PlaintextRenderer.

HTTP client

Client over AsyncHTTPClient with futures and async overloads.

One requirement, send(_:) async throws; a Duration timeout (30 seconds default), maxResponseBodySize of 10 MB, a body type shared with Response.Body.

Commands, logging, tracing

ConsoleKit 4 AsyncCommand, LoggingSystem.bootstrap(from:).

ConsoleKit 5 (beta), the ConsoleLogger module (bootstrapWithConfigReader(config:)), a task-local logger (withLogger, Logger.current).

Testing

Application.make(.testing), app.testing().test(...), promises available.

New VaporTesting APIs; the promise type is removed.

A Vapor 5 application

The scratch package below builds against 5.0.0-beta.4 with no warnings. The entrypoint is the shape of the repository’s own Development target (it has no Environment.detect() or Application.make), the controller uses the @Controller macro, and the plain route reads the request body as a stream.

Package.swift:

// swift-tools-version:6.4
import PackageDescription

let package = Package(
    name: "bookshelf-v5",
    platforms: [.macOS("26.2")],
    dependencies: [
        .package(url: "https://github.com/vapor/vapor.git", exact: "5.0.0-beta.4"),
        .package(url: "https://github.com/apple/swift-configuration.git", from: "1.0.0", traits: ["CommandLineArguments"]),
    ],
    targets: [
        .executableTarget(
            name: "App",
            dependencies: [
                .product(name: "Vapor", package: "vapor"),
                .product(name: "VaporMacros", package: "vapor"),
                .product(name: "Configuration", package: "swift-configuration"),
            ]
        )
    ]
)

entrypoint.swift:

import Configuration
import ConsoleLogger
import Logging
import Vapor

@main
struct Entrypoint {
    static func main() async throws {
        let config = ConfigReader(providers: [
            CommandLineArgumentsProvider(),
            EnvironmentVariablesProvider(),
        ])
        ConsoleLogger.bootstrapWithConfigReader(config: config)
        let logger = Logger(label: "codes.vapor.bookshelf")
        try await withLogger(logger) { _ in
            let app = try Application(configReader: config)
            do {
                try await configure(app)
                try await app.run()
                try await app.shutdown()
            } catch {
                try? await app.shutdown()
                throw error
            }
        }
    }
}

configure.swift:

import Vapor

func configure(_ app: Application) async throws {
    app.serverConfiguration.address = .hostname("127.0.0.1", port: 8089)
    try await routes(app)
}

routes.swift:

import HTTPTypes
import Vapor
import VaporMacros

struct BookDTO: Content {
    var id: Int
    var title: String
}

@Controller
struct BookController {
    @GET("books")
    func list(req: Request) async throws -> [BookDTO] {
        [BookDTO(id: 1, title: "Swift for Servers")]
    }

    @GET("books", Int.self)
    func show(req: Request, bookID: Int) async throws -> BookDTO {
        BookDTO(id: bookID, title: "Book \(bookID)")
    }
}

func routes(_ app: Application) async throws {
    app.get("hello") { _ in "Hello, Vapor 5" }

    app.on(.post, "echo") { req -> String in
        var total = 0
        try await req.body.forEachChunk { chunk in
            total += chunk.count
        }
        return "received \(total) bytes"
    }

    try await app.register(collection: BookController())
}

Source: vapor/vapor 5.0.0-beta.4: Sources/Development (the entrypoint, the route registration and the macro controller are adapted from it)

Running it and calling it:

$ swift run App
$ curl -s -i localhost:8089/hello | head -4
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 14

$ curl -s localhost:8089/books
[{"title":"Swift for Servers","id":1}]
$ curl -s localhost:8089/books/42
{"id":42,"title":"Book 42"}
$ head -c 100000 /dev/zero | curl -s -X POST --data-binary @- localhost:8089/echo
received 100000 bytes

The @Controller macro generates the RouteCollection conformance and boot(routes:), with one @Sendable registration per annotated method; a path component given as a type (Int.self) becomes a typed parameter handed to the function. Expanded for the first method, the macro’s own tests show:

extension UserController: RouteCollection {
    func boot(routes: any RoutesBuilder) throws {
        routes.get("api", "macros", "users") { req async throws -> Response in
            try await self._route_getUsers(req: req)
        }
    }
}

The other macros are @POST, @PUT, @DELETE, @PATCH and @HTTP(method, ...) (and freestanding forms for use inside function bodies), @AuthMiddleware(User.self, ...) and, new in beta.4, #Middleware(using:) { ... } and #AuthMiddleware(...) { ... } blocks that wrap the routes declared inside them. The Development target of the tag has runnable examples that supply the middleware’s dependencies by key path, by factory closure or from a task-local.

Ecosystem readiness

Checked on 2026-10-17 against the default branches and tags of the repositories.

Package Latest tag Vapor 5 status

fluent, fluent-kit

4.13.0, 1.57.0

main of fluent depends on Vapor from 4.117.0. fluent-kit has a fluent-5 branch. No Fluent release targets Vapor 5; the blog’s plan says Fluent 5 is a separate effort and that FluentKit 4 will probably work with Vapor 5; no tagged release confirms it yet.

leaf, leaf-kit

4.5.2, 1.14.3

main of leaf depends on Vapor from 4.121.3. leaf-kit has a v5 branch.

jwt, jwt-kit

5.1.2, 5.7.1

main of jwt depends on Vapor from 4.110.2; there is only a v4 branch besides it.

queues, queues-redis-driver

1.18.0, 1.1.2

main of queues depends on Vapor from 4.104.0; no Vapor 5 branch.

redis

4.14.0

main depends on Vapor from 4.100.0; a v5-dev branch and a 5.0.0-alpha.2.2 pre-release tag exist, with no release notes yet.

apns

5.1.0

A vapor5 branch targets Vapor 5.0.0-beta.3 and newer with the same APNSwift.

swift-openapi-vapor

1.2.0 and 2.0.0-beta.1

2.0.0-beta.1 depends on Vapor 5.0.0-beta.4.

console-kit, routing-kit, multipart-kit

5.0.0-beta.2, 5.0.0-beta.3, 5.0.0-beta.2

Betas used by Vapor 5; the 4.x stable lines serve Vapor 4.

fluent-mongo-driver, Imperial, VaporSecurityHeaders

1.4.0 (2024-10), 2.2.0, 4.2.0

No Vapor 5 release found.

A Vapor application that needs Fluent, Leaf, JWT, Queues or Redis cannot move to Vapor 5 today, which is the strongest reason to stay on 4.x for anything that has to ship.

Migration advice

Moving within Vapor 4:

  1. Move the entrypoint to Application.make, execute() and asyncShutdown(), and remove Application(env), run() and shutdown() (4.113 deprecation).

  2. Convert handlers, middleware, migrations and commands to their async protocols; keep EventLoopFuture only inside third-party code, bridged with .get().

  3. Turn on the Swift 6 language mode with swift-tools-version:6.3 and fix Sendable problems at the source: @unchecked Sendable on Fluent models, Sendable storage values, @Sendable handlers.

  4. Replace XCTest with VaporTesting and Swift Testing (4.110), using withApp so that asyncShutdown() always runs.

  5. Move .sql(raw:) and \(raw:) to unsafeRaw: and read the other deprecation warnings that a Swift 6.4 build prints.

Preparing for Vapor 5 without waiting for it:

  1. Keep the application’s own code behind small protocols: a configuration type (AppConfig) that is the only reader of Environment, a password hasher, a file store, and repositories over Fluent. These are the parts that Vapor 5 changes or removes.

  2. Return DTOs, not Fluent models, and keep Content types independent of Request.

  3. Write streaming code with async bodies (req.body, Response.Body(managedAsyncStream:)) rather than collecting everything.

  4. Do not depend on app.storage, app.locks, Environment.get, req.fileio.writeFile, .env files or OTP in code you expect to carry over.

  5. Try the beta in a branch with a throwaway service to learn the new application shape, and re-run it on every beta.

Moving to Vapor 5, once it is released and the packages you use have Vapor 5 releases: read the official upgrade guide (it covers only Vapor 3 to 4 today) and the release notes of 5.0.0, rewrite the entrypoint and configure around ConfigReader, pass the services in ServiceConfiguration, change AsyncMiddleware conformances to Middleware, await content decoding and validation, replace NIO types by swift-http-types and Foundation types, and register routes with the macros if you want type-safe parameters.

Where the books are outdated

All three books are outdated: Patel (2018) is written for Vapor 2, Yeung (2018) for Vapor 3.0 and Swift 4.2, and the Kodeco book of Condon and others is the early-access build whose chapters 1 to 13 use Vapor 4.0 on Swift 5.2 and whose chapters 14 to 36 still show Vapor 3. The books are consulted references for concepts and narrative; no text, code or figure is copied from them, and none of their code compiles against Vapor 4.122 as printed. Their ISBNs, publishers and companion repositories are in the section bibliography.

By area

Each row says what the books teach, with the Vapor or Swift version in which it changed, and where the current form is.

Area Outdated in the books Current form and page

Whole framework

Patel: Vapor 2 (Droplet, JSON Config/*.json, Providers, Node/JSONConvertible, ResourceRepresentable). Yeung and Condon ch. 14 to 36: Vapor 3 (Services, Config, config.prefer, router.get, req.parameters.next(T.self), Future<T>, req.make, PostgreSQLModel).

Vapor 4: Application, app.get, req.parameters.get(_:as:), Content, Fluent 4 property wrappers, app.databases, app.migrations, app.middleware, app.views. See the tables above and Introduction and architecture.

Concurrency

EventLoopFuture chains and wait() (Condon ch. 4 to 13).

async handlers, AsyncMigration, AsyncMiddleware, AsyncCommand, AsyncJob, AsyncBasicAuthenticator; futures only as legacy with .get(). Swift 6 strict concurrency: @Sendable route closures, Sendable services and storage, final class ...: Model, @unchecked Sendable. See Swift concurrency in Vapor.

Entrypoint

main.swift with app(.detect()).run(), Application(env) or a Run target (4.x deprecations, noasync).

@main enum Entrypoint, Application.make(env), app.execute(), app.asyncShutdown(), LoggingSystem.bootstrap(from:), executable target App (swift run App serve). See Application lifecycle and entrypoint.

Toolchain

Swift 4 to 5.2, Xcode 9 to 11, Ubuntu 14.04 to 18.04, the Vapor APT repository, check.vapor.sh, vapor build/run/xcode/cloud/heroku, swift package generate-xcodeproj.

Swift 6.x with Swiftly or Docker, Toolbox 20 (vapor new only), open Package.swift in Xcode or VS Code. See Installation and toolchain.

SwiftPM

LinuxMain.swift and allTests, --enable-test-discovery (obsolete since Swift 5.4), the -cross-module-optimization unsafe flag.

Automatic test discovery, current tools versions, upcoming-feature flags. See Project structure and SwiftPM.

Fluent

Fluent 2 and 3 (Entity, Row, Preparation, SQLiteModel, extension ...: Migration {}, Parameter), deletedAtKey, alsoDecode, withPooledConnection.

Model property wrappers, @Timestamp(on: .delete), with(\.$rel) eager loading, SQLKit raw SQL with bound interpolation, paginate, .postgres(configuration: SQLPostgresConfiguration(...)) with explicit TLS. See Fluent models, relations, migrations and queries.

Leaf

Leaf 2 and 3: #embed, #set/#get, #loop, brace blocks, LeafProvider, req.make(LeafRenderer.self).

Leaf 4: #extend/#export/#import, #if(...):, #for(...):, app.views.use(.leaf), req.view.render. See Leaf templates and custom tags and HTML DSLs.

Authentication

vapor/auth and auth-provider (PasswordAuthenticatable, TokenAuthenticatable, authSessionsMiddleware, PersistMiddleware, BCryptDigest).

Built-in ModelAuthenticatable, ModelTokenAuthenticatable, ModelSessionAuthenticatable, ModelCredentialsAuthenticatable, req.auth, Bcrypt, req.password, app.sessions; JWT 5, which no book covers. See Authentication, Fluent authentication and tokens and JWT.

Caching and Redis

KeyedCache, MemoryKeyedCache, the Redis provider with jsonSet and jsonGet.

app.caches.use(.memory/.fluent/.redis), req.cache, vapor/redis 4 (RediStack), app.sessions.use(.redis). See Redis and caching.

Files

DirectoryConfig and req.streamFile (Vapor 3).

req.fileio (streamFile, writeFile, async FileIO), body: .collect(maxSize:) or .stream, app.directory, and object storage (S3 through Soto) for multi-instance deployments. See Static files and file I/O and File uploads and streaming.

Testing

Droplet.testable(), Vapor 3 Request(http:using:), XCTVapor only.

VaporTesting with Swift Testing (@Test, #expect, app.testing().test(...), withApp) since 4.110; XCTVapor is legacy.

OAuth and email

Imperial 0.7 (Vapor 3); the vapor-community SendGrid provider.

Imperial 2.x for Vapor 4; sendgrid-kit/sendgrid, Mailgun 6 and SMTP packages; Sign in with Apple through req.jwt.apple. See OAuth and Sign in with Apple.

Deployment

Vapor Cloud (shut down in 2020), Heroku’s free tier (ended in November 2022), the heroku-16 stack, .swift-version files with Swift 4.x, mLab (shut down in 2020), the Travis CI free tier, swift:4.2 and ubuntu:16.04 images.

The official multi-stage swift:6.x-noble to ubuntu:noble Dockerfile (static standard library, jemalloc, non-root user, backtraces), Fly.io, DigitalOcean, systemd or Supervisor with nginx, ECS and Fargate, Kubernetes, GitHub Actions. See Docker, deployment targets and production.

Clients

UIKit with storyboards, completion-handler URLSession, tokens in UserDefaults, global ATS exceptions, models duplicated in the app.

SwiftUI, async URLSession.data(for:), the Keychain, ATS exceptions limited to localhost in Debug builds, and a shared DTO Swift package. See Full-Stack Swift with iOS Clients.

Frameworks

Kitura (IBM ended support in 2019), Perfect (inactive), IBM Cloud ibmcloud cs.

Hummingbird 2 is the main alternative. Ecosystem and Framework Comparison compares them.

By book and chapter

The chapter numbers of the Kodeco book follow its early-access build, the one cited in the bibliography; the final edition adds a Sign in with Apple chapter that the early-access build lacks, so its later chapters are numbered one higher.

Book and chapters What it teaches What is outdated or unsafe, and the current form Page

Condon 1 and 3

Vapor history and team, SwiftNIO, HTTP basics.

If the edition predates 4.50, handlers return EventLoopFuture. The concepts hold; the code needs the async rewrite. Nothing on Swift 6 strict concurrency or Vapor 5.

Introduction and architecture

Condon 2

Toolbox, vapor new, SwiftPM layout, first routes, Abort, decoding Content.

swift run Run and a Sources/Run target became swift run App; vapor xcode and generate-xcodeproj are gone; check.vapor.sh and the APT repository are obsolete.

Getting started

Condon 4

Non-blocking I/O, futures and promises, map/flatMap/flatMapThrowing/transform/and/flatten, error recovery, the dangers of wait().

The operators remain accurate but are legacy; new code uses async/await, and wait() is flagged inside async code.

Swift concurrency in Vapor

Condon 5 to 7

Models, migrations, autoMigrate, SQLite, MySQL, PostgreSQL and MongoDB, Environment.get, Docker databases, CRUD, filter/group(.or)/sort/first, search by query parameters.

Futures instead of async; autoMigrate().wait() in configure is replaced by try await app.autoMigrate(); models are not Sendable; models double as Content; MySQL certificateVerification: .none; fallback database credentials in code; a search that interpolates a user-supplied sort string is unsafe (whitelist it). .sql(raw:) is deprecated.

Fluent overview, models, migrations, queries

Condon 8 to 10

RouteCollection, groups, @Parent/@Children, foreign keys, DTOs, @Siblings and pivots, attach/detach, cascade deletes.

Future chains; boot(router:) is boot(routes:); use .ifNotExists for idempotent attach; add onDelete: .cascade where children must go with parents; measure queries to avoid N+1.

Controllers, relations

Condon 11

XCTVapor, .testing, app.test, a separate test database, Linux testing with Docker Compose.

VaporTesting and Swift Testing since 4.110; the synchronous app.test is unavailable from async tests.

Testing

Condon 12, 13 and 19

An iOS client for the API.

Completion handlers, duplicated models, tokens in UserDefaults (use the Keychain), global ATS exceptions. The modern form is an async client actor and a shared package.

Full-Stack Swift with iOS Clients

Condon 14 to 17

Leaf contexts, tags, base templates, Bootstrap, FileMiddleware, forms, redirects.

Still Vapor 3 and Leaf 3 in the early-access build: #embed/#set/#get, req.view().render, LeafProvider. Forms without CSRF protection, validation messages reflected through the query string, deletes over GET.

Leaf, forms, static files

Condon 18 and 20

Password hashing, User.Public, HTTP Basic, tokens, guards, admin seeding; sessions, web login and logout, redirect middleware, CSRF tokens, cookie consent.

A seeded admin with the password "password"; plaintext tokens that never expire; a logout as a link; the session ID kept across login (rotate it). BCryptDigest and the Vapor 3 services are gone.

Authentication, tokens, sessions, web login, security

Condon 21

Validatable, built-in and custom validators.

The validations(_:) signature; the error message is one joined string, so use the structured failures for an API.

Validation

Condon 22 and 23 (plus the final edition’s Sign in with Apple chapter)

OAuth with Imperial, Sign in with Apple.

Imperial 0.x is future-based and for Vapor 3; JWTSigners and app.jwt.signers are jwt 4; the redirect carries no state in Imperial 2.2.0; the application identifier check is opt-in.

OAuth and Sign in with Apple

Condon 24 and 25

Unique email, password-reset tokens, transactional email; multipart uploads.

A Vapor 3 SendGrid provider and futures; reset tokens stored in plaintext without expiry; the profile-picture upload takes the user ID from the URL with no ownership check and no size or type validation; whole-file .collect(maxSize:) holds the upload in memory; files written under the client’s name allow path traversal.

Sending Email, File uploads and streaming

Condon 26

Schema-changing migrations, API versioning, environment-specific seeding.

MigrationConfig and the Vapor 3 Migration forms; the versioning idea holds.

Migrations, and API design

Condon 27

Caching with memory, database and Redis; cache-aside with an external API through Client.

Vapor 3 services and futures; check a database-backed cache against Fluent’s no-expiry, no-overwrite behaviour.

Redis and caching, HTTP client

Condon 28

Custom middleware, shared-secret header, route-group middleware.

MiddlewareConfig and futures; the shared secret compared with == must use a constant-time comparison.

Middleware

Condon 29

WebSockets, session registries, a live-location client.

The registry is a plain class shared across event loops (a data race); use an actor. The Vapor 3 server service is gone.

WebSockets

Condon 30

Soft delete and restore, timestamps, enums, admin authorization, lifecycle hooks, eager loading, joins, raw SQL.

Early-access text on Vapor 3 and Swift 4.2; rewrite with @Timestamp(on: .delete), AsyncModelMiddleware, with and join, and SQLKit with unsafeRaw:.

Transactions, model middleware and soft delete, relations, SQLKit, authorization

Condon 31 to 34

Heroku, Docker and Compose, AWS EC2 and RDS with nginx and systemd, environments, release builds, Supervisor, scaling, Redis sessions.

Heroku’s free tier ended; sudo su - and wget | apt-key add; OAuth secrets set on the command line; the official multi-stage Dockerfile replaces hand-built images.

Docker, deployment targets and production

Condon 35 and 36

Monolith versus microservices, per-service databases, token validation through a users service, a gateway proxying with Client.

Tokens in plaintext; service-to-service credentials over plain HTTP; a gateway that copies headers wholesale (use an allow-list); no timeouts.

HTTP client, and microservices

Yeung 1

Swift on Linux, SwiftNIO EventLoop/Channel/ChannelPipeline, Vapor versus Kitura versus Perfect.

Kitura and Perfect are no longer developed; Vapor 3’s Future and Container are gone.

Introduction and architecture

Yeung 2 to 4 and appendix A

Toolbox commands, boilerplate, the old services container, debugging in Xcode, logging levels, testing, coverage, Postman and curl.

vapor xcode and old Toolbox flags; Services and Config; Run target.

Getting started, Project structure

Yeung 5 to 7

Content structs, an in-memory CRUD controller, route collections, Leaf partials, Bootstrap through a CDN with SRI.

A mutable array in a controller shared across event loops (a data race); 400 for not found and 200 for create; "HTTP UPDATE"; logout and delete over GET; Leaf 3 syntax.

Content, controllers, Leaf

Yeung 8 and 9

Postgres, SQLite and MySQL, Fluent drivers, session-based admin login with bcrypt, redirect middleware.

Typos (hotname:), fallback credentials, published bcrypt hashes for seeding, a Content-conforming user leaking its hash, forms without CSRF protection, logout over GET.

Fluent overview, web login

Yeung 10, 11

Three-tier architecture, Backends for Frontends, API gateway, REST resource modelling, pagination query parameters.

Vapor 3 Router and Future; an unauthenticated admin API; 400 for not found; plain-HTTP service calls without timeouts.

API design, microservices, HTTP client

Yeung 12, 14

Cloud deployment (Vapor Cloud), containers, registries, Kubernetes.

Vapor Cloud no longer exists; IBM Cloud commands are historical.

Docker, deployment targets and production

Yeung 13

An iPhone CRUD client over the API, ATS exceptions.

Global "Allow Arbitrary Loads"; use NSAllowsLocalNetworking in a Debug-only plist.

Full-Stack Swift with iOS Clients

Patel 1

Swift scripts, SwiftPM library and executable packages, a raw-socket HTTP server, #if os(Linux).

A stored property in an extension and non-public package API do not compile; reach for NIO’s ServerBootstrap or Vapor, not hand-written sockets.

Project structure, Introduction

Patel 2, 6 and 11

An iOS app evolving from local persistence to an API client, one workspace for app and server, a tvOS target sharing code.

Vapor 2 and Xcode 9; #if TARGET_OS_IOS is always false in Swift (use #if os(iOS)); global ATS exceptions; tokens in UserDefaults.

Full-Stack Swift with iOS Clients

Patel 3 and 4

Vapor 2 packages, Droplet, JSON configuration, Providers, engine-level HTTP and WebSocket servers, writing a provider, MongoDB.

None of it exists in Vapor 4. Default crypto.json keys, a static file server that allowed path traversal, MongoDB with access control disabled, non-public API in the provider package.

Application lifecycle, Services and storage, MongoDB

Patel 5

REST verbs, nested and wildcard routes, resource controllers, PATCH.

An unauthenticated DELETE that wipes a table; N+1 queries in serialisation; no cascade on parent delete; 400 for not found.

Routing, and API design

Patel 7

Leaf layouts, content negotiation, custom middleware, AJAX CRUD.

Leaf 2 syntax; a template name taken from the URL; no CSRF token on the browser interface; no Vary header.

Content, forms, Middleware

Patel 8 and 9

Environment-specific configuration, response assertions, CI on Linux and macOS, Heroku with a managed database.

Travis CI (use GitHub Actions); sudo chmod -R a+rx /usr/ in CI; Heroku’s free tier is gone.

Configuration, and testing, deployment

Patel 10

Password and session authentication for the web and bearer tokens for apps on one backend, token issuing behind Basic auth, iOS login.

SHA-256 shown before bcrypt; plaintext tokens; tokens in UserDefaults; Vapor 2 Auth.

Tokens, web login, passwords

Errata and bad practices not to reproduce

The books contain code that is wrong or unsafe independently of their age. This section’s pages do not reproduce it, and say the correct form where the topic comes up.

Category What the books do The correct form here

Secrets and credentials

Hard-coded fallback database credentials such as ?? "vapor_password" (Condon ch. 6, 11; Yeung ch. 8); a seeded admin with the password "password" (Condon ch. 18); bcrypt hashes published for seeding (Yeung ch. 9); OAuth secrets set on the command line (Condon ch. 31); default crypto.json keys (Patel ch. 3 and 4).

Read secrets from the environment and fail fast in production; create the admin with a command that takes the password from the operator’s environment and hashes it at run time. See Configuration and environment.

TLS

certificateVerification: .none for MySQL (Condon ch. 6); global ATS "Allow Arbitrary Loads" (Patel ch. 6, Yeung ch. 13); service-to-service credentials over plain HTTP (Condon ch. 36).

Keep certificate verification on (turn it off only for a local container, never through a downgraded URL); narrow ATS exceptions in Debug builds; TLS between services you do not run on a trusted network. See Server configuration and HTTP client.

Tokens and passwords

Bearer and password-reset tokens stored in plaintext with no expiry (Condon ch. 18, 24, 35; Patel ch. 10); SHA-256 password hashing shown before bcrypt (Patel ch. 10); a Content-conforming user model leaking the hash to JSON and templates (Yeung ch. 9); tokens in UserDefaults (Condon ch. 19, Patel ch. 10).

Store a digest of the token with an expiry; use req.password; return a public DTO; use the Keychain. See Fluent authentication and tokens and Passwords and crypto.

Authorization

A profile-picture upload taking the user ID from the URL with no ownership check and no size or type validation (Condon ch. 25); an unauthenticated admin API (Yeung ch. 11); an unauthenticated DELETE that wipes a table (Patel ch. 5); a shared-secret header compared with == (Condon ch. 28).

Derive the owner from the authenticated user; guard every admin route; compare secrets with secureCompare or an HMAC. See Authorization.

Web security

Forms without CSRF protection, or with a token on only one form (Condon ch. 16, 20; Yeung ch. 9; Patel ch. 7); logout and deletes over GET (Yeung ch. 6, 9; Patel ch. 10); validation messages reflected through the query string (Condon ch. 21); a template name taken from the URL (Patel ch. 7); a static file server allowing path traversal (Patel ch. 3).

A CSRF token, SameSite and an origin check on every cookie-authenticated form; POST for state changes; messages kept in the session; templates chosen in code. See Security hardening and Server-rendered web apps and forms.

Concurrency

A mutable in-memory array in a controller shared across event loops (Yeung ch. 5); a global mutable WebSocket registry (Condon ch. 29); autoMigrate().wait() on an event loop (Condon ch. 5).

An actor for shared state; try await app.autoMigrate(). See Swift concurrency in Vapor and WebSockets.

HTTP semantics

400 for not found and 200 for creation (Yeung ch. 5, 11; Patel ch. 5); "HTTP UPDATE" (Yeung ch. 5); N+1 queries in serialisation (Patel ch. 5); no cascade on parent delete (Patel ch. 5).

404 for a missing record, 201 with Location for creation, 204 for a successful delete; eager loading with with; onDelete: .cascade. See Error handling.

Operations

sudo su - and wget | apt-key add (Condon ch. 33); sudo chmod -R a+rx /usr/ in CI (Patel ch. 8); MongoDB with access control disabled (Patel ch. 4).

A non-root service user, signed repositories, least privilege in CI, and authentication enabled on every database. See Deployment with Docker.

Typos and broken code

hotname:, extra parentheses and wrong module names (Yeung ch. 8); a stored property in an extension and non-public package API (Patel ch. 1, 4); #if TARGET_OS_IOS, always false in Swift (Patel ch. 11); a "ttps://" source-code link and the word "releational" (Condon).

Compiling examples; #if os(iOS).

What the books do not cover

None of the three books, and for several topics not even the official 4.0 documentation, covers the following. The pages of this section fill the gap from the sources:

Common errors when upgrading

Symptom Cause and fix

'init(_:_:)' is deprecated: Migrate to using the async APIs

Application(env) in an entrypoint copied from a book or the upgrade guide. Use try await Application.make(env).

instance method 'test' is unavailable from asynchronous contexts

The synchronous XCTVapor app.test called in an async test. Use try await app.test(...) or VaporTesting.

.unknownCommand("boot", ...)

There is no boot command in 4.122.2. Use routes or a custom command (see Commands).

Warnings about Sendable after switching to tools 6.3

Models without @unchecked Sendable, a non-Sendable value under a StorageKey, a non-@Sendable closure that captures req. Fix them; do not switch back to the Swift 5 mode.

expression is 'async' but is not marked with 'await' on app.register(collection:)

Vapor 5 beta only: registration is async there.

Package resolution fails for 5.0.0-beta.4

The beta needs tools 6.4 and 26.2 Apple platforms, and dependencies that are themselves prereleases; pin exact: and build with a Swift 6.4 toolchain.

A Fluent or Leaf package will not resolve next to Vapor 5

Those packages still depend on Vapor 4; see Ecosystem readiness.