Loading...
Loading...
Vapor framework guardrails, patterns, and best practices for AI-assisted development. Use when working with Vapor projects, or when the user mentions Vapor. Provides Fluent ORM, async Swift, routing, middleware, and server-side Swift guidelines.
npx skill4agent add ar4mirez/samuel vaporApplies to: Vapor 4.x, Swift 5.9+, Fluent ORM, Leaf Templates, Server-Side Swift Language Guide: @.claude/skills/swift-guide/SKILL.md
@mainApplication.makeRouteCollection@ID@Field@Parent@ChildrenContentValidatableAsyncMiddlewareAsyncMigrationpreparerevert@Sendable@unchecked SendableautoMigratereverttry!fatalErrorMyVaporApp/
├── 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.ymlControllers/Services/Models/DTOs/ContentValidatableMigrations/preparerevertMiddleware/// 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())
}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)
}
}RouteCollection@Sendableimport 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
}
}final classModelContent@unchecked Sendable@ID(key: .id)@Timestampcreated_atupdated_at@Parent@Children@Siblingsinit()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()
}
}preparerevertrevert.references()onDeleteimport 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
}
}ContentValidatableContenttry CreateUserRequest.validate(content: req)Validatable.email.count(range)!.empty.url.alphanumericimport 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)
}
}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
}Contentconfigure.swiftlet encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.dateEncodingStrategy = .iso8601
ContentConfiguration.global.use(encoder: encoder, for: .json)// 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)# 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| Package | Purpose |
|---|---|
| Core web framework |
| ORM abstraction |
| PostgreSQL support |
| SQLite (development/testing) |
| Redis caching and sessions |
| JWT authentication |
| Template engine |
| Testing utilities (included with Vapor) |