vapor

Vapor Framework Guide

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "vapor" with this command: npx skills add ar4mirez/samuel/ar4mirez-samuel-vapor

Vapor Framework Guide

Applies to: Vapor 4.x, Swift 5.9+, Fluent ORM, Leaf Templates, Server-Side Swift Language Guide: @.claude/skills/swift-guide/SKILL.md

Overview

Vapor is a server-side Swift framework for building web applications, REST APIs, and backend services. It provides type-safe routing, Fluent ORM, async/await concurrency, JWT authentication, and Leaf templating.

Use Vapor when:

  • Building Swift-native backend services

  • Sharing code between iOS/macOS clients and the server

  • You need type-safe, compile-time-checked API development

  • You want async/await patterns throughout the stack

Consider alternatives when:

  • Team lacks Swift experience

  • You need a massive middleware ecosystem (consider Express, Rails)

  • Maximum raw performance is critical (consider Rust/Actix-web)

Guardrails

Vapor-Specific Rules

  • Use the @main entry point pattern with Application.make

  • Group routes by resource with RouteCollection controllers

  • Use Fluent property wrappers (@ID , @Field , @Parent , @Children ) for models

  • Use Content protocol for all request/response DTOs

  • Use Validatable protocol for input validation on every endpoint

  • Use AsyncMiddleware for cross-cutting concerns (auth, logging, CORS)

  • Use AsyncMigration with both prepare and revert methods

  • Configure databases from environment variables (never hardcode credentials)

  • Use DTOs to separate API contracts from database models

  • Implement pagination for all list endpoints

  • Use @Sendable on all route handler closures

  • Mark model classes as @unchecked Sendable (Fluent requirement)

Anti-Patterns

  • Do not expose Fluent models directly as API responses (use DTOs)

  • Do not put business logic in controllers (use a service layer)

  • Do not use autoMigrate in production (run migrations explicitly)

  • Do not skip revert in migrations (always provide rollback)

  • Do not use try! or fatalError in request handlers

  • Do not store request-scoped state in global variables

Project Structure

MyVaporApp/ ├── Package.swift ├── Sources/ │ └── App/ │ ├── Controllers/ # RouteCollection implementations │ │ ├── UserController.swift │ │ └── AuthController.swift │ ├── Models/ # Fluent models │ │ ├── User.swift │ │ └── Post.swift │ ├── DTOs/ # Request/response types (Content + Validatable) │ │ ├── UserDTO.swift │ │ └── CreateUserRequest.swift │ ├── Migrations/ # AsyncMigration implementations │ │ ├── CreateUser.swift │ │ └── CreatePost.swift │ ├── Middleware/ # AsyncMiddleware implementations │ │ ├── JWTAuthMiddleware.swift │ │ └── AppErrorMiddleware.swift │ ├── Services/ # Business logic (protocol + implementation) │ │ ├── UserService.swift │ │ └── EmailService.swift │ ├── Extensions/ │ │ └── Request+Extensions.swift │ ├── configure.swift # Database, middleware, JWT, Leaf setup │ ├── routes.swift # Top-level route registration │ └── entrypoint.swift # @main entry point ├── Tests/ │ └── AppTests/ │ ├── UserControllerTests.swift │ └── AuthControllerTests.swift ├── Resources/ │ └── Views/ # Leaf templates │ └── index.leaf ├── Public/ # Static files │ ├── css/ │ └── js/ └── docker-compose.yml

Layer responsibilities:

  • Controllers/ -- HTTP routing only: parse request, call service, write response

  • Services/ -- Business logic, orchestration, domain rules

  • Models/ -- Fluent database models with property wrappers

  • DTOs/ -- Request/response types with validation (Content

  • Validatable )
  • Migrations/ -- Schema changes with prepare and revert

  • Middleware/ -- Cross-cutting: auth, error handling, CORS, logging

Application Setup

Entry Point and Configuration

// entrypoint.swift import Vapor import Logging

@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 } } }

// configure.swift -- database, middleware, migrations, routes func configure(_ app: Application) async throws { app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory)) app.middleware.use(AppErrorMiddleware()) app.middleware.use(CORSMiddleware())

// Database (always from environment variables)
if let databaseURL = Environment.get("DATABASE_URL") {
    try app.databases.use(.postgres(url: databaseURL), as: .psql)
} else {
    app.databases.use(.postgres(
        hostname: Environment.get("DB_HOST") ?? "localhost",
        port: Environment.get("DB_PORT").flatMap(Int.init) ?? 5432,
        username: Environment.get("DB_USER") ?? "vapor",
        password: Environment.get("DB_PASSWORD") ?? "vapor",
        database: Environment.get("DB_NAME") ?? "vapor_dev"
    ), as: .psql)
}

app.migrations.add(CreateUser())
if app.environment == .development { try await app.autoMigrate() }
try routes(app)

}

// routes.swift -- group public vs protected func routes(_ app: Application) throws { app.get("health") { _ -> HTTPStatus in .ok } let api = app.grouped("api", "v1") try api.register(collection: AuthController())

let protected = api.grouped(JWTAuthMiddleware())
try protected.register(collection: UserController())

}

Routing with Controllers

struct UserController: RouteCollection { func boot(routes: RoutesBuilder) throws { let users = routes.grouped("users") users.get(use: index) users.get(":userID", use: show) users.put(":userID", use: update) users.delete(":userID", use: delete) }

@Sendable
func index(req: Request) async throws -> PaginatedResponse<UserResponse> {
    let page = try req.query.decode(PageRequest.self)
    let result = try await User.query(on: req.db)
        .filter(\.$isActive == true)
        .sort(\.$createdAt, .descending)
        .paginate(PageRequest(page: page.page, per: page.per))
    let items = try result.items.map { try UserResponse(user: $0) }
    return PaginatedResponse(items: items, metadata: PageMetadata(
        page: page.page, perPage: page.per,
        total: result.metadata.total, totalPages: result.metadata.pageCount
    ))
}

@Sendable
func show(req: Request) async throws -> UserResponse {
    guard let user = try await User.find(req.parameters.get("userID"), on: req.db) else {
        throw Abort(.notFound, reason: "User not found")
    }
    return try UserResponse(user: user)
}

}

Conventions: Implement RouteCollection per resource. Use @Sendable on all handlers. Validate before processing. Return DTOs, not Fluent models.

Fluent Models

Model with Property Wrappers

import Fluent import Vapor

final class User: Model, Content, @unchecked Sendable { static let schema = "users"

@ID(key: .id)
var id: UUID?

@Field(key: "email")
var email: String

@Field(key: "password_hash")
var passwordHash: String

@Enum(key: "role")
var role: Role

@Timestamp(key: "created_at", on: .create)
var createdAt: Date?

@Timestamp(key: "updated_at", on: .update)
var updatedAt: Date?

@Children(for: \.$user)
var posts: [Post]

init() {}

init(id: UUID? = nil, email: String, passwordHash: String, role: Role = .user) {
    self.id = id
    self.email = email
    self.passwordHash = passwordHash
    self.role = role
}

enum Role: String, Codable, CaseIterable {
    case admin, user, guest
}

}

Model conventions:

  • Always mark as final class conforming to Model , Content , @unchecked Sendable

  • Use @ID(key: .id) for UUID primary keys

  • Use @Timestamp for created_at and updated_at

  • Use @Parent /@Children /@Siblings for relationships

  • Provide an empty init() (Fluent requirement)

Migrations

import Fluent

struct CreateUser: AsyncMigration { func prepare(on database: Database) async throws { let role = try await database.enum("user_role") .case("admin").case("user").case("guest") .create()

    try await database.schema("users")
        .id()
        .field("email", .string, .required)
        .field("password_hash", .string, .required)
        .field("role", role, .required)
        .field("created_at", .datetime)
        .field("updated_at", .datetime)
        .unique(on: "email")
        .create()
}

func revert(on database: Database) async throws {
    try await database.schema("users").delete()
    try await database.enum("user_role").delete()
}

}

Migration rules:

  • Always implement both prepare and revert

  • Create enums before referencing them in schema

  • Delete enums in revert after deleting the table

  • Use .references() for foreign keys with onDelete behavior

  • Add indexes for frequently queried columns

DTOs and Validation

Request/Response DTOs

import Vapor

struct CreateUserRequest: Content, Validatable { let email: String let password: String let name: String

static func validations(_ validations: inout Validations) {
    validations.add("email", as: String.self, is: .email)
    validations.add("password", as: String.self, is: .count(8...))
    validations.add("name", as: String.self, is: !.empty)
}

}

struct UserResponse: Content { let id: UUID let email: String let name: String let role: User.Role let createdAt: Date?

init(user: User) throws {
    self.id = try user.requireID()
    self.email = user.email
    self.name = user.name
    self.role = user.role
    self.createdAt = user.createdAt
}

}

DTO conventions:

  • Request types conform to Content
  • Validatable
  • Response types conform to Content only

  • Always validate in the controller before processing: try CreateUserRequest.validate(content: req)

  • Use Validatable rules: .email , .count(range) , !.empty , .url , .alphanumeric

Middleware

Custom AsyncMiddleware

import Vapor import JWT

struct JWTAuthMiddleware: AsyncMiddleware { func respond( to request: Request, chainingTo next: any AsyncResponder ) async throws -> Response { guard let token = request.headers.bearerAuthorization?.token else { throw Abort(.unauthorized, reason: "Missing authorization token") }

    let payload = try await request.jwt.verify(token, as: UserPayload.self)

    guard let userID = UUID(payload.subject.value),
          let user = try await User.find(userID, on: request.db),
          user.isActive else {
        throw Abort(.unauthorized, reason: "User not found or inactive")
    }

    request.auth.login(user)
    return try await next.respond(to: request)
}

}

Error Middleware

import Vapor

struct AppErrorMiddleware: AsyncMiddleware { func respond( to request: Request, chainingTo next: any AsyncResponder ) async throws -> Response { do { return try await next.respond(to: request) } catch let abort as AbortError { let body = ErrorResponse(error: true, reason: abort.reason, code: abort.status.code) return try await body.encodeResponse(status: abort.status, for: request) } catch { request.logger.error("Unexpected error: (error)") let reason = request.application.environment.isRelease ? "An internal error occurred" : error.localizedDescription let body = ErrorResponse(error: true, reason: reason, code: 500) return try await body.encodeResponse(status: .internalServerError, for: request) } } }

struct ErrorResponse: Content { let error: Bool let reason: String let code: UInt }

Content Negotiation

Vapor's Content protocol handles JSON automatically. Configure custom encoding in configure.swift :

let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase encoder.dateEncodingStrategy = .iso8601 ContentConfiguration.global.use(encoder: encoder, for: .json)

Authentication (JWT)

// configure.swift -- register signing key guard let jwtSecret = Environment.get("JWT_SECRET") else { fatalError("JWT_SECRET environment variable not set") } await app.jwt.keys.add(hmac: HMACKey(from: jwtSecret), digestAlgorithm: .sha256)

// Payload definition struct UserPayload: JWTPayload { var subject: SubjectClaim var expiration: ExpirationClaim var isAdmin: Bool?

func verify(using algorithm: some JWTAlgorithm) throws {
    try expiration.verifyNotExpired()
}

}

// Token generation (in login handler) let payload = UserPayload( subject: .init(value: try user.requireID().uuidString), expiration: .init(value: Date().addingTimeInterval(3600)) ) let token = try await req.jwt.sign(payload)

Commands Reference

Initialize project

swift package init --type executable --name MyVaporApp

Resolve dependencies

swift package resolve

Build and run

swift build swift run App serve --hostname 0.0.0.0 --port 8080

Run tests

swift test swift test --filter AppTests

Run database migrations manually

swift run App migrate swift run App migrate --revert

Docker build

docker build -t my-vapor-app . docker compose up -d

Dependencies

Package Purpose

vapor/vapor

Core web framework

vapor/fluent

ORM abstraction

vapor/fluent-postgres-driver

PostgreSQL support

vapor/fluent-sqlite-driver

SQLite (development/testing)

vapor/redis

Redis caching and sessions

vapor/jwt

JWT authentication

vapor/leaf

Template engine

XCTVapor

Testing utilities (included with Vapor)

Advanced Topics

For detailed patterns, WebSocket integration, Leaf templates, queues, testing, and deployment, see:

  • references/patterns.md -- Fluent query patterns, relationships, eager loading, WebSocket, Leaf templates, background queues, comprehensive testing, Docker deployment

External References

  • Vapor Documentation

  • Fluent Documentation

  • Vapor Discord

  • Swift Server Workgroup

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

actix-web

No summary provided by upstream source.

Repository SourceNeeds Review
General

frontend-design

No summary provided by upstream source.

Repository SourceNeeds Review
General

blazor

No summary provided by upstream source.

Repository SourceNeeds Review