Introduction & Architecture

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 is written against Vapor 4.122.2 (the latest 4.x release, published 2026-09-17) on Swift 6 (the local toolchain is Swift 6.4; the latest swift.org release at the time of writing is 6.4.1), with SwiftNIO 2.104, swift-log 1.16, RoutingKit 4.9 and ConsoleKit 4.x, checked on 2026-10-17. Vapor 4’s own Package.swift declares swift-tools-version:6.0 and the official vapor/template uses tools version 6.3, so every example here compiles in the Swift 6 language mode. Vapor 5 exists as a beta (5.0.0-beta.4, 2026-10-13) and is not generally available: differences that are already visible in it are called out as upcoming, never as the current behaviour.

What Vapor is

Vapor is an open-source web framework for Swift, released under the MIT licence and maintained by the Vapor organisation on GitHub. The framework’s documentation describes it as a way to build websites, APIs and cloud services with Swift’s type safety and expressiveness, and three properties shape everything else in this section:

  • Asynchronous and non-blocking. Vapor does not start a thread per request. A small number of threads (by default one per CPU core) each run an event loop that multiplexes thousands of connections. Your handlers are async functions that suspend while waiting for a database or an HTTP call instead of holding a thread.

  • Swift-native. Routes, middleware, models and configuration are ordinary Swift types: structs that conform to Content are request and response bodies, Codable does the JSON encoding, property wrappers describe database columns, protocols define extension points, and the compiler checks the result. There is no reflection-driven dependency injection container and no code generation step.

  • Built on SwiftNIO. The networking layer is Apple’s SwiftNIO, the same library that powers grpc-swift, AsyncHTTPClient and many other server-side Swift packages. Vapor adds the HTTP-framework layer on top: the router, middleware, content negotiation, sessions, authentication, validation, commands and a services mechanism for plugging in everything else.

Vapor is a framework rather than a library of parts: the Application object owns the event loop group, the logger, the router, the middleware stack, the command-line interface and the lifecycle, and the other packages of the ecosystem (Fluent, Leaf, Queues, JWT, Redis) register themselves on it. The section landing page lists every topic that follows; this page gives the map you need to place each of them.

A short history

The dates below come from the repository and its GitHub releases:

Version First release What defined it

(repository)

2016-01-18

The vapor/vapor repository is created. Swift itself had been open source for a few weeks and had no official Linux web story; early Vapor is one of the first server-side frameworks.

Vapor 1

2016-09-15 (1.0.0)

The first stable release, built on its own HTTP and socket stack before SwiftNIO existed.

Vapor 2

2017-05-17 (2.0.0)

Second major version, still on the pre-NIO stack (applications were built around the Droplet).

Vapor 3

2018-05-04 (3.0.0)

Rewritten on SwiftNIO with a new future/promise API (Future<T>), which introduced the EventLoopGroup model Vapor still uses.

Vapor 4

2020-04-09 (4.0.0)

Redesigned API: Application and Request own services, EventLoopFuture replaces Future, Fluent 4 with property-wrapper models. async/await support arrived in 4.50.0 (2021-10-26) and is what the current template uses. Today at 4.122.2.

Vapor 5

beta since 2026-09-15

Swift 6.4 tools, new server abstraction, Response and Request overhauls and macro-based routing. Betas 1 to 4 were published between 2026-09-15 and 2026-10-13; not GA.

Two conclusions matter when you read older material. First, anything written for Vapor 3 (Future<T>, Container, Service registrations in configure(_:_:_:)) does not compile on Vapor 4. Second, much Vapor 4 material predates Swift concurrency and builds every handler around EventLoopFuture; that style still works and is labelled legacy in this section, but new code uses async/await.

Why server-side Swift

Swift on the server is not a replacement for every backend stack, but it has a distinct set of strengths:

  • One language end to end. If your product has an iOS or macOS client, the same Codable model types can be shared between app and server through a Swift package. A change to Book is a compile error on both sides instead of a runtime decoding failure. The Codable & Serialization page covers the encoding rules both sides rely on.

  • Performance and memory. Swift compiles to native code with no garbage collector; memory is managed with automatic reference counting (ARC). A typical Vapor service starts quickly, idles at tens of megabytes and has a flat latency tail, which makes it a good citizen in containers.

  • Type safety. Optionals make absent values explicit, enums with associated values model states, generics keep routes and Fluent queries typed, and since Swift 6 the compiler also rules out data races in the language mode (see Actors, Isolation & Sendable).

  • Modern concurrency. async/await, structured concurrency and actors give straight-line asynchronous code (see Async/Await & Tasks).

  • A real ecosystem. The Swift Server Workgroup (SSWG) incubates the shared packages — logging, metrics, tracing, HTTP clients, database drivers — so Vapor, Hummingbird and others interoperate.

It is not always the right choice. Weigh these before committing a team to it:

  • Hiring and community size. The pool of engineers who know Vapor is far smaller than for Spring, Django, Express or ASP.NET Core, and third-party integrations (payment providers, CMS SDKs) usually ship Java, Python or JavaScript libraries first. Expect to write more thin HTTP clients yourself.

  • Build times and tooling. Swift compiles slowly compared with Go, and a full Vapor build from clean takes minutes. Incremental builds are fine; Docker images need layer caching to stay pleasant.

  • Linux is a second-class development target for Apple tooling. You can develop on macOS and Linux, but Foundation differences and Apple-only frameworks (SwiftUI, CoreData, Combine) are unavailable on Linux, so server code must stick to portable APIs.

  • Framework churn. Vapor 3 to 4 was a rewrite and Vapor 5 will change APIs again; plan upgrade time.

  • Team already productive elsewhere. If nobody on the team writes Swift and there are no Apple clients to share models with, a mainstream stack is almost always cheaper. The Starting a New Project page gives the general decision framework.

A good fit is therefore: a team that already writes Swift for Apple platforms and wants shared models end to end, JSON APIs for iOS and macOS apps, push-notification senders, and server-rendered sites with Leaf.

The SwiftNIO foundation

SwiftNIO is an event-driven, non-blocking network application framework. It is the reason Vapor can be both fast and small, and a handful of its concepts appear in Vapor’s own API, so learn them before the rest of the section.

Concept What it is and where Vapor shows it

EventLoop

The basic I/O primitive: a single thread that waits for events (readable socket, timer, scheduled task) and runs callbacks. Work on most NIO objects must happen on their loop to be thread-safe. Vapor exposes it as req.eventLoop; each request is served by exactly one loop.

EventLoopGroup

A set of event loops that spreads connections among them. MultiThreadedEventLoopGroup defaults to one loop per core (System.coreCount). Vapor exposes it as app.eventLoopGroup; the default provider is NIO’s shared singleton group.

Channel

Owns a file descriptor (a listening socket or one accepted TCP connection), its lifetime and its inbound and outbound events. A Vapor server holds one server Channel and one child Channel per connection.

ChannelPipeline

An ordered chain of handlers attached to a Channel. Inbound events (reads) flow from the head to the tail; outbound events (writes) flow from the tail to the head.

ChannelHandler

One processing step: TLS, an HTTP codec, a compressor, a timeout. Each handler does one thing and can be reused in other servers.

ByteBuffer

NIO’s copy-on-write byte container. Vapor request and response bodies are streams of ByteBuffer.

EventLoopFuture / EventLoopPromise

The pre-concurrency result type. Still everywhere in the API, but async/await overloads exist for nearly all of it.

NIOThreadPool

A pool of ordinary threads for work that cannot be made non-blocking. Vapor exposes it as app.threadPool.

A pipeline in miniature

The smallest useful NIO program is a TCP server whose pipeline has one handler. It upper-cases whatever it receives, which makes the roles of ServerBootstrap, the childChannelInitializer and the handler visible:

import NIOCore
import NIOPosix

/// One pipeline stage: upper-cases every inbound ByteBuffer and writes it back.
final class ShoutHandler: ChannelInboundHandler {
    typealias InboundIn = ByteBuffer
    typealias OutboundOut = ByteBuffer

    func channelRead(context: ChannelHandlerContext, data: NIOAny) {
        var input = unwrapInboundIn(data)
        let text = input.readString(length: input.readableBytes) ?? ""
        let output = context.channel.allocator.buffer(string: text.uppercased())
        context.writeAndFlush(wrapOutboundOut(output), promise: nil)
    }

    func errorCaught(context: ChannelHandlerContext, error: any Error) {
        context.close(promise: nil)
    }
}

let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)

let bootstrap = ServerBootstrap(group: group)
    .serverChannelOption(.socketOption(.so_reuseaddr), value: 1)
    .childChannelInitializer { channel in
        channel.eventLoop.makeCompletedFuture {
            try channel.pipeline.syncOperations.addHandler(ShoutHandler())
        }
    }

let channel = try await bootstrap.bind(host: "127.0.0.1", port: 9100).get()
print("listening on \(channel.localAddress!)")
try await channel.closeFuture.get()
try await group.shutdownGracefully()

Notice what the example does not do: it never creates a thread per connection and never blocks. The childChannelInitializer runs once for every accepted connection and installs that connection’s pipeline; channelRead runs on the connection’s event loop whenever bytes arrive. Vapor’s HTTP server is exactly this, with a longer pipeline.

How Vapor’s HTTP server sits on NIO

When you run app.execute() with the default serve command, Vapor’s HTTPServer binds a NIO ServerBootstrap on app.eventLoopGroup. For each accepted connection its child-channel initializer builds a pipeline. The Vapor 4.122.2 source assembles this pipeline for an HTTP/1.1 connection, in order:

  1. HTTPResponseEncoder and a ByteToMessageHandler(HTTPRequestDecoder(...)) from NIOHTTP1 — bytes to HTTP parts and back. (For TLS, NIOSSL’s NIOSSLServerHandler sits in front; for HTTP/2, NIOHTTP2 frames are converted to HTTP/1 parts first.)

  2. Optionally an HTTPServerPipelineHandler when supportPipelining is on.

  3. The response compressor and, if enabled, a request decompressor (HTTPServer.Configuration.responseCompression, requestDecompression).

  4. HTTPServerResponseEncoder — turns a Vapor Response into HTTP parts, adding Server and Date headers.

  5. HTTPServerRequestDecoder — turns HTTP parts into a Vapor Request, buffering or streaming the body.

  6. A HTTPServerUpgradeHandler (WebSocket upgrades) and finally HTTPServerHandler, the only handler that knows about the framework: it calls responder.respond(to: request) and writes the result.

  7. NIOCloseOnErrorHandler closes the connection on any unhandled pipeline error.

Everything above HTTPServerHandler is NIO; everything below it — routing, middleware, your handlers — is Vapor. That handoff point is the Responder protocol, and it is the centre of the lifecycle described below.

Why blocking an event loop is fatal

An event loop is one thread serving many connections. If a handler running on that thread blocks — Thread.sleep, a synchronous file or network read, DispatchSemaphore.wait(), a .wait() on an EventLoopFuture, a long CPU loop — every other connection assigned to that loop stalls with it: no bytes are read, no responses are written, no timers fire. On an 8-core machine, eight such requests freeze the whole server, however light the load looks. SwiftNIO’s documentation and Vapor’s both say the same thing: do not block; move blocking work off the loop.

Swift concurrency changes the details but not the rule. A Vapor async handler starts on the request’s event loop and resumes on the cooperative thread pool after its first suspension, so a blocking call inside it can freeze either an event loop or a Swift concurrency thread (the cooperative pool has roughly one thread per core, and Swift’s concurrency model assumes that tasks make forward progress). The three options, from best to worst, are:

import Vapor

func routes(_ app: Application) throws {
    // Wrong: a synchronous closure runs on the event loop and parks it for two seconds.
    app.get("blocking", "wrong") { _ -> String in
        Thread.sleep(forTimeInterval: 2)
        return "done (blocked a thread)"
    }

    // Right (1): a non-blocking suspension; the thread is free while waiting.
    app.get("blocking", "async") { _ async throws -> String in
        try await Task.sleep(for: .seconds(2))
        return "done (suspended)"
    }

    // Right (2): unavoidable blocking work goes to the NIO thread pool.
    app.get("blocking", "pool") { req async throws -> String in
        try await req.application.threadPool.runIfActive {
            Thread.sleep(forTimeInterval: 2)
            return "done (on the thread pool)"
        }
    }
}

Rules of thumb that follow:

  • Use the async API of every library (Fluent, Redis, req.client, req.fileio); they are non-blocking by design.

  • Wrap synchronous work (a C library, a PDF renderer, a password hash with a large cost factor) in app.threadPool.runIfActive { ... } or a dedicated actor.

  • Never call .wait() on an EventLoopFuture from a request handler; Vapor crashes in debug builds with a precondition failure from NIO when you try it on an event loop. .wait() is only acceptable in the main thread’s setup code, and the Application.make entrypoint avoids it altogether.

  • Hashing with Bcrypt is deliberately slow; Vapor offers req.password.async(...) so that it runs on the thread pool (see the Authentication and Passwords and Crypto pages).

Layers of the stack

Diagram of the Vapor stack as five layers. From the bottom: operating system sockets (non-blocking TCP multiplexed with kqueue or epoll); SwiftNIO (MultiThreadedEventLoopGroup, EventLoop, ServerBootstrap, Channel, ChannelPipeline, ByteBuffer, NIOThreadPool); the Vapor HTTP server (pipeline handlers for TLS, HTTP codecs and compression, HTTPServerRequestDecoder that builds a Request, HTTPServerHandler that calls the Responder, plus Application services); the router (DefaultResponder with a RoutingKit TrieRouter lookup); the middleware chain (RouteLoggingMiddleware, ErrorMiddleware, application middleware, group middleware); and the route handler on top. Requests flow up the stack and responses flow down.
Layer Responsibility

Operating system

Non-blocking sockets, readiness notification through kqueue (macOS, BSD) or epoll (Linux).

SwiftNIO

Event loops, channels and pipelines, byte buffers, futures, the thread pool. NIOCore has the abstractions, NIOPosix the POSIX implementations, NIOHTTP1 the HTTP/1 codec, NIOWebSocket the WebSocket codec; swift-nio-ssl adds TLS and swift-nio-http2 adds HTTP/2.

Vapor HTTP server

HTTPServer and its configuration (app.http.server.configuration): hostname, port, TLS, HTTP/2, compression, request size limit, idle timeout. Creates Request and Response values from the pipeline.

Router

DefaultResponder, the Responder the server calls. It owns a TrieRouter from the RoutingKit package and chooses the route by method and path components.

Middleware

Types conforming to Middleware or AsyncMiddleware that wrap a Responder in another Responder.

Handler

Your closure or controller method. Returns anything that is ResponseEncodable: String, a Content struct, a Response, an HTTPStatus, Void.

One subtlety separates Vapor from some other frameworks: matching the route and running middleware are not two separate passes. DefaultResponder looks up the route first and, at start-up, has already wrapped every route’s responder in the global middleware chain (middleware.makeResponder(chainingTo: route.responder)), so a request that matches nothing is also sent through the same middleware, chained to a "not found" responder. That is why ErrorMiddleware can turn an unknown URL into a JSON 404 response and why a custom logging middleware sees unmatched requests too.

The request lifecycle

The sequence below follows one GET /books/9780441013593 request through the Bookshelf service. Every other core page has a short "Where it runs in the request" section that places its topic on this diagram:

sequenceDiagram autonumber participant C as Client participant L as Server Channel (NIO) participant P as Connection pipeline participant S as HTTPServerHandler participant R as DefaultResponder (router) participant M as Middleware chain participant H as Route handler C->>L: TCP connect L->>P: accept, assign an EventLoop, build the pipeline C->>P: GET /books/9780441013593 (bytes) P->>P: TLS, HTTP codec, decompression P->>S: Vapor Request (HTTPServerRequestDecoder) S->>R: responder.respond(to: request) R->>R: TrieRouter lookup: GET + books + :isbn R->>M: matched route (or 404 responder) wrapped in middleware M->>M: RouteLogging, Error, A, B, group middleware C M->>H: handler(request) H->>H: decode content, async work (Fluent, Redis ...) H-->>M: ResponseEncodable becomes a Response M-->>R: Response (each middleware sees it on the way out, errors become responses) R-->>S: Response S-->>P: write Response P-->>C: HTTP/1.1 200 OK + JSON body
  1. Accept. The server Channel accepts a connection. NIO assigns it to one EventLoop of app.eventLoopGroup for its whole lifetime, and Vapor’s child-channel initializer builds the pipeline shown above.

  2. Decode. The pipeline parses TLS, HTTP and compression and HTTPServerRequestDecoder creates a Request, whose body is either collected (up to app.routes.defaultMaxBodySize, 16 KB by default) or streamed, depending on the route’s body: strategy.

  3. Hand-off. HTTPServerHandler calls the Responder stored in app.responder, by default DefaultResponder.

  4. Route lookup. The TrieRouter matches the method and path. Matched :name parameters are stored in req.parameters; the matched Route is stored on req.route.

  5. Middleware. The request goes through the default stack, RouteLoggingMiddleware (which logs the method and path) and then ErrorMiddleware, then the middleware added with app.middleware.use(_:), then middleware from route groups — the order in which they were registered. Any of them may short-circuit and return a response, or throw.

  6. Handler. Your async throws code decodes input (req.content, req.query, req.parameters), does its work and returns a value. The value is turned into a Response by encodeResponse(for:).

  7. Response path. The response travels back through the middleware in reverse order. If anything threw, ErrorMiddleware converts the error (an Abort, a DebuggableError, or any other error) into a response with a status code and, in development, a reason.

  8. Encode and write. HTTPServerResponseEncoder serialises the status, headers and body; the compressor and the HTTP codec turn them into bytes; the keep-alive connection waits for the next request.

Seeing the middleware order

The following application registers two global middleware and one group middleware, and logs a line on the way in and the way out of each. The group middleware, as the Vapor documentation states, always runs after the application-wide middleware:

import Vapor

/// Writes one line to the log on the way in and one on the way out.
struct TraceMiddleware: AsyncMiddleware {
    let name: String

    func respond(to request: Request, chainingTo next: any AsyncResponder) async throws -> Response {
        request.logger.info("-> \(name)")
        let response = try await next.respond(to: request)
        request.logger.info("<- \(name)")
        return response
    }
}

struct Book: Content {
    let isbn: String
    let title: String
}

func configure(_ app: Application) async throws {
    // The default stack is RouteLoggingMiddleware, then ErrorMiddleware. Ours are appended after them: A, then B.
    app.middleware.use(TraceMiddleware(name: "A (app)"))
    app.middleware.use(TraceMiddleware(name: "B (app)"))
    try routes(app)
}

func routes(_ app: Application) throws {
    app.get("health") { _ in "ok" }

    // Group middleware always runs after the application-wide middleware.
    let books = app.grouped("books").grouped(TraceMiddleware(name: "C (group)"))
    books.get(":isbn") { req async throws -> Book in
        req.logger.info("handler for \(req.url.path)")
        let isbn = try req.parameters.require("isbn")
        return Book(isbn: isbn, title: "Dune")
    }
}

Running the server and calling curl http://127.0.0.1:8080/books/9780441013593 logs, in this order (the default RouteLoggingMiddleware also logs a GET /books/... line first; the request-id metadata suffix and the lines of other requests are trimmed):

-> A (app)
-> B (app)
-> C (group)
handler for /books/9780441013593
<- C (group)
<- B (app)
<- A (app)

The response body is {"isbn":"9780441013593","title":"Dune"}. Calling an unknown URL such as /nope runs -> A and -> B but logs no <- lines, because the not-found responder throws Abort(.notFound) and the exception unwinds the chain until ErrorMiddleware turns it into the response {"reason":"Not Found","error":true} with status 404. The order A -> B -> C -> handler -> C -> B -> A is exactly the one the Vapor documentation draws. To run a middleware before the default ErrorMiddleware — as the CORS middleware must, so that error responses carry CORS headers — register it with app.middleware.use(cors, at: .beginning).

Where it runs in the request

This page is the map the other pages refer to: NIO accepts the connection and builds the pipeline, the HTTP server creates a Request, DefaultResponder matches a route, middleware runs around it, the handler runs, and the response travels back out the same way. The later pages each pin their topic to one of those steps; for example, content decoding (Content and Codable) and validation (Validation) belong to the handler step, authentication (Authentication) to the middleware step (Middleware), and Fluent (Fluent Overview and Database Drivers) to work done inside the handler.

The package map

Vapor is not one repository. The Vapor product lives in vapor/vapor and depends on several small packages that you can also use on their own. The table lists the ones this section uses, with the version tags checked on 2026-10-17 (the Vapor 4 line):

Package Latest tag Role

vapor/vapor

4.122.2

The framework: Application, Request, Response, Content, middleware, sessions, Validations, the HTTP server and client (HTTP Client), commands, testing support (VaporTesting, XCTVapor; see Testing)

vapor/routing-kit

4.9.3

The trie-based router and PathComponent/Parameters machinery behind app.get("books", ":isbn") (Routing)

vapor/console-kit

4.x (5.0.0-beta.2 for Vapor 5)

Command-line interface: Command, Console, the serve, routes, migrate and queues commands (Commands)

vapor/multipart-kit

4.7.1

multipart/form-data parsing and serialisation for uploads (File Uploads and Streaming)

vapor/websocket-kit

2.16.2

WebSocket client and server on NIOWebSocket (WebSockets)

vapor/async-kit

1.22.0

Connection pools and EventLoopFuture helpers used by the database drivers

vapor/fluent and vapor/fluent-kit

4.13.0 and 1.57.0

The ORM: models, queries, relations, migrations (fluent is the Vapor integration, fluent-kit the framework-independent core)

vapor/fluent-postgres-driver, fluent-sqlite-driver, fluent-mysql-driver, fluent-mongo-driver

2.14.0, 4.9.0, 4.8.0, 1.4.0

Database drivers for Fluent (MongoDB for the Mongo driver)

vapor/sql-kit

3.36.0

A database-agnostic SQL query builder and serializer used by the SQL drivers (SQLDatabase for raw SQL, see SQLKit and Raw SQL)

vapor/leaf and vapor/leaf-kit

4.5.2 and 1.14.3

The Leaf templating language (leaf integrates, leaf-kit parses and renders; see Leaf Templates)

vapor/redis

4.14.0

Redis client (RediStack) integration, sessions and cache (Redis & Caching)

vapor/queues and vapor/queues-redis-driver

1.18.0 and 1.1.2

Background jobs and scheduled jobs (Queues & Background Jobs, Scheduled Jobs)

vapor/jwt and vapor/jwt-kit

5.1.2 and 5.7.1

JSON Web Token signing and verification (JSON Web Tokens (JWT))

vapor/apns (with APNSwift)

5.1.0 (APNSwift 7.1.0)

Apple Push Notification service client (APNs Push Notifications)

The pages that follow add each dependency to a Package.swift with a pinned from: version. The Vapor documentation only has a section for the packages it maintains; the Toolbox (vapor new) is a separate tool, version 20.0.2, covered on the Installation and Toolchain page (the first project is created on Getting Started).

The Swift Server Workgroup packages Vapor builds on

Vapor deliberately does not invent logging, metrics, tracing, HTTP-client or cryptography APIs. It depends on the shared packages promoted by the Swift Server Workgroup (SSWG) and Apple, so that a library written against them works with Vapor, Hummingbird or a plain NIO service. From the dependency list of Vapor 4.122.2:

Package Used by Vapor for

swift-log (1.16.1)

The Logger API. req.logger and app.logger carry request metadata (request-id); you pick the backend with LoggingSystem.bootstrap.

swift-metrics (2.12)

The Metrics API for counters and timers. DefaultResponder emits http_requests_total and a duration timer when a metrics backend is bootstrapped.

swift-distributed-tracing (1.7)

The Tracing API and service context propagation across await points.

AsyncHTTPClient (1.36)

req.client and app.client, Vapor’s outbound HTTP client.

swift-crypto (5.0)

SHA-2, HMAC, AES and random bytes, a portable CryptoKit implementation for Linux; also used by JWT and sessions.

swift-nio, swift-nio-ssl, swift-nio-http2, swift-nio-extras

The networking base: pipelines, TLS, HTTP/2 and compression handlers.

swift-algorithms

Collection algorithms used internally.

The versions in parentheses are the latest tags of each package; Vapor’s Package.swift states only minimum versions (swift-log from 1.8.0, swift-metrics from 2.5.0, AsyncHTTPClient from 1.24.0, swift-crypto in 1.0.0..<5.0.0), and SwiftPM resolves the newest compatible ones. Later pages show how to bootstrap the logging (Logging), metrics and tracing (Tracing, Metrics & OpenTelemetry) backends.

Vapor, Hummingbird and the older frameworks

Vapor’s main contemporary is Hummingbird (version 2.28.0 at the time of writing), a lighter framework that is also based on SwiftNIO, Swift concurrency and the SSWG packages. In one paragraph: Hummingbird is modular and minimal — the core is a router, middleware and a server, and you add persistence, templating and authentication from separate packages — and its current version 2 is built on async/await; Vapor is batteries-included, with its own ORM, template language, queue system, command framework and a much larger community and documentation. Choose Vapor when you want an integrated stack and the largest amount of tutorials and third-party packages; look at Hummingbird for small services or when you want to assemble the pieces yourself. A feature-by-feature comparison, with migration notes, is in the Ecosystem and Framework Comparison page.

Kitura (IBM) and Perfect (PerfectlySoft) were the other early Swift server frameworks, from the Swift 2 and 3 era. Both remain on GitHub, but their repositories show no recent development (the last code commits on the default branches are from 2022 for Kitura and February 2021 for Perfect), they target old Swift versions and they do not take advantage of SwiftNIO-native async APIs in the way Vapor and Hummingbird do. Treat them as history: books from that era (Yeung’s, for instance, compares all three) explain why Vapor won, but you should not start a new project on either.

The running example: Bookshelf

Every page uses the same domain, the original Bookshelf application shared by the other backend sections of this documentation: a library of books and authors, readers who borrow them, reviews, and notifications for due dates.

Diagram of the Bookshelf application: iOS and macOS apps, browsers and API clients reach a reverse proxy that terminates TLS and balances load, then the Bookshelf service built on Vapor 4.122 with a SwiftNIO HTTP server, middleware and router, controllers and handlers, and Fluent models and services. The service uses PostgreSQL through Fluent, Redis for sessions cache and queues, S3 object storage for book covers, APNs for push notifications and an email provider. A separate Queues worker from the same codebase handles emails, push notifications and scheduled overdue checks asynchronously.

The service is a single Swift package with one executable target. Requests enter through the NIO HTTP server, the middleware and router select a controller, controllers use Fluent models and plain Swift services, and slow side effects — emails, push notifications, overdue checks — are enqueued and processed by a second process started from the same binary with the queues command. The next pages build it up step by step.

The complete mind-map of this section follows; use it to find where each topic is covered:

mindmap root((Swift Vapor)) Foundations Introduction and architecture Installation and toolchain Getting started Project structure and SwiftPM Application lifecycle Swift concurrency in Vapor Requests and responses Routing and controllers Request and response Content and Codable Validation and errors Middleware Data Fluent models and queries Relations and migrations Databases and Redis Web UI Leaf templates Forms and sessions Static files and WebSockets Security Passwords and sessions JWT and OAuth Authorization and hardening APIs and integration REST and OpenAPI HTTP client APNs and email Queues and background jobs Quality and operations Testing with VaporTesting Logging, metrics and tracing Docker, Linux and deployment Performance Reference Cheat sheet Bibliography

Common errors and pitfalls

  • A request hangs under light load; top shows idle CPUs. An event loop is blocked. Look for Thread.sleep, synchronous disk or network calls, semaphore.wait() or .wait() in handlers, and move them as shown above.

  • A handler that mutates a captured var and compiles with only a warning. Route closures are declared @Sendable, but Vapor 4.122’s RoutesBuilder methods are marked @preconcurrency, so even in the Swift 6 language mode the compiler reports a handler that captures and mutates a local var as a warning ("mutation of captured var in concurrently-executing code"), not an error. The data race is still real, because handlers run concurrently on many event loops. Keep shared state in an actor, a Sendable type with a lock, or the storage of the Application, and read it from req.application.

  • Log line Application.shutdown() was not called before Application deinitialized. An Application must be shut down before it is deallocated: call try await app.asyncShutdown() on every exit path of main, as the template’s entrypoint does. Otherwise Vapor logs this error from deinit and falls back to a blocking shutdown.

  • CORS headers are missing from error responses. The CORS middleware was registered after ErrorMiddleware; use app.middleware.use(cors, at: .beginning).

  • 404 instead of the response you expected, for every route. The routes(_:) function was never called from configure(_:), or the route’s path components do not match; run the routes command (swift run App routes) to print the registered table.

  • Code from a Vapor 3 tutorial does not compile. Future<T>, Container, Service and Router were replaced in Vapor 4; see the upgrading guide linked in the references.

  • .wait() crashes in a handler. NIO refuses to block an event loop’s thread with .wait(); the fix is the async overload or .get().

  • Works on macOS, fails on Linux. A Foundation or Apple-only API was used; build in Docker (swift:6.x image) early, and see the Installation and Toolchain page for the supported Linux distributions (images and deployment: Deployment with Docker).

Version notes

  • Vapor 3 to 4. A rewrite: Application and Request now carry the services; EventLoopFuture replaces the Vapor-specific Future; Fluent models use property wrappers (@ID, @Field); Content (a Codable specialisation) describes request and response bodies.

  • Vapor 4.50 and later. 4.50.0 (2021-10-26) added async/await APIs for Client, RoutesBuilder, ResponseEncodable, Responder, Middleware (AsyncMiddleware), AsyncPasswordHasher, Cache and views; more followed in later releases. Application.make (an async factory) and app.asyncShutdown() are what the current template’s @main enum Entrypoint uses. In 4.122.2 Application, Request and the Middleware protocol are Sendable.

  • Vapor 5 (beta). Visible on the main branch and in 5.0.0-beta.4: tools version 6.4 and Apple platform minimums of 26.2, optional SwiftPM traits (WebSockets, HTTPClient, Multipart, MacroRouting), swift-configuration for settings, macro-based routing, a server abstraction replacing the NIO pipeline above, NIO types removed from the public Response API and a Request overhaul, plus ConsoleKit 5 and RoutingKit 5 betas (details on the What’s New, Upgrading and Vapor 5 page). The page notes where these matter; examples target the stable 4.x line.

Where the books are outdated

The three books consulted for this section (Condon et al., Yeung and Patel) were written while Vapor 3 or early Vapor 4 was current. For the concepts on this page:

  • Condon et al. cover Vapor’s team and history and the HTTP basics; if your edition predates Vapor 4.50 (October 2021), its handlers return EventLoopFuture. The concepts (EventLoop, pipeline) are unchanged, the code needs the async rewrite shown here.

  • Yeung’s chapter on server-side Swift compares Vapor with Kitura and Perfect as live alternatives; Kitura and Perfect are no longer actively developed, and Vapor 3’s Future and Container are gone in Vapor 4.

  • Patel’s early chapter builds a raw-socket HTTP server and a SwiftPM library to explain how servers work; the idea is sound, but today you would reach for NIO’s ServerBootstrap (as above) or Vapor rather than hand-written sockets.

  • None of them cover Swift 6 strict concurrency (Sendable closures), Application.make, or the Vapor 5 beta.