Skip to content

What it is

Vapor is the leading server-side Swift web framework, built on SwiftNIO with routing, middleware, an ORM and authentication.

Routes are closures registered on the Application. Content conformance handles decoding and encoding, and Fluent provides the ORM layer.

Installation

.package(url: "https://github.com/vapor/vapor.git", from: "4.106.0")

Getting started

The smallest useful thing you can do with it, and what each part means.

Routes and content
struct CreateBook: Content, Validatable {
    let title: String
    let year: Int

    static func validations(_ v: inout Validations) {
        v.add("title", as: String.self, is: !.empty && .count(...200))
        v.add("year", as: Int.self, is: .range(1400...2100))
    }
}

func routes(_ app: Application) throws {
    app.get("books", ":id") { req async throws -> Book in
        guard let id = req.parameters.get("id", as: UUID.self) else {
            throw Abort(.badRequest, reason: "invalid id")
        }
        guard let book = try await Book.find(id, on: req.db) else {
            throw Abort(.notFound)
        }
        return book
    }

    app.post("books") { req async throws -> Response in
        try CreateBook.validate(content: req)
        let input = try req.content.decode(CreateBook.self)
        let book = Book(title: input.title, year: input.year)
        try await book.save(on: req.db)
        return try await book.encodeResponse(status: .created, for: req)
    }
}
Abort carries an HTTP status, so throwing it from anywhere produces the right response — no need to thread error handling back through the route closure.

Advanced usage

Where the library earns its place over a simpler alternative.

Middleware and migrations
struct APIKeyMiddleware: AsyncMiddleware {
    func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response {
        guard request.headers.first(name: "X-API-Key") == expectedKey else {
            throw Abort(.unauthorized)
        }
        return try await next.respond(to: request)
    }
}

let protected = app.grouped(APIKeyMiddleware())
protected.get("admin", "stats") { req in try await Stats.current(on: req.db) }

struct CreateBookTable: AsyncMigration {
    func prepare(on database: Database) async throws {
        try await database.schema("books")
            .id()
            .field("title", .string, .required)
            .field("year", .int, .required)
            .unique(on: "title", "year")
            .create()
    }
    func revert(on database: Database) async throws {
        try await database.schema("books").delete()
    }
}
Route groups apply middleware to a whole section, so a new admin endpoint is protected by default rather than by remembering to add the middleware.

Errors and fixes

The failures you are most likely to hit, and what actually resolves them.

Value of type Request has no member db
Fluent is not configured. Add the driver package and call app.databases.use in configure.
The server blocks under load
Synchronous work on the event loop. Move blocking calls to a thread pool or make them async.

Best practices

  • Use route groups for authentication so protection is the default for a section.
  • Write both prepare and revert in migrations; one you cannot roll back is a liability.
  • Use async/await routes rather than the older EventLoopFuture API.
  • Be realistic about ecosystem size — choose Vapor when the team is already Swift.

Background

Why it exists, and what it was reacting to.

Vapor makes it practical to write a back end in Swift, which appeals to iOS teams wanting one language across client and server. It is mature, though its ecosystem is far smaller than Node's or Java's.