Skip to content

Vapor Framework Guide

Framework: Vapor 4.x Language: Swift 5.9+ Type: Server-Side Web Framework Platform: Linux, macOS


Overview

Vapor is a popular server-side Swift framework for building web applications, APIs, and backend services. It provides a type-safe, expressive API with async/await support.

Use Vapor when: - Building Swift-native backend services - Need type-safe API development - Sharing code between iOS/macOS apps and backend - Building with async/await patterns - Want Swift package ecosystem integration

Consider alternatives when: - Team lacks Swift experience - Need extensive middleware ecosystem - Require specific database support not available - Need maximum raw performance (consider C++/Rust)


Project Structure

MyVaporApp/
├── Package.swift
├── Sources/
│   └── App/
│       ├── Controllers/
│       │   ├── UserController.swift
│       │   └── AuthController.swift
│       ├── Models/
│       │   ├── User.swift
│       │   └── Post.swift
│       ├── DTOs/
│       │   ├── UserDTO.swift
│       │   └── CreateUserRequest.swift
│       ├── Migrations/
│       │   ├── CreateUser.swift
│       │   └── CreatePost.swift
│       ├── Middleware/
│       │   ├── AuthMiddleware.swift
│       │   └── ErrorMiddleware.swift
│       ├── Services/
│       │   ├── UserService.swift
│       │   └── EmailService.swift
│       ├── Extensions/
│       │   └── Request+Extensions.swift
│       ├── configure.swift
│       ├── routes.swift
│       └── entrypoint.swift
├── Tests/
│   └── AppTests/
│       ├── UserControllerTests.swift
│       └── AuthControllerTests.swift
├── Resources/
│   └── Views/
│       └── index.leaf
├── Public/
│   ├── css/
│   └── js/
└── docker-compose.yml

Package.swift

// swift-tools-version:5.9
import PackageDescription

let package = Package(
    name: "MyVaporApp",
    platforms: [
        .macOS(.v13)
    ],
    dependencies: [
        // Vapor
        .package(url: "https://github.com/vapor/vapor.git", from: "4.89.0"),
        // Fluent ORM
        .package(url: "https://github.com/vapor/fluent.git", from: "4.9.0"),
        // PostgreSQL driver
        .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.8.0"),
        // Redis
        .package(url: "https://github.com/vapor/redis.git", from: "4.10.0"),
        // JWT authentication
        .package(url: "https://github.com/vapor/jwt.git", from: "4.2.0"),
        // Leaf templating
        .package(url: "https://github.com/vapor/leaf.git", from: "4.3.0"),
    ],
    targets: [
        .executableTarget(
            name: "App",
            dependencies: [
                .product(name: "Vapor", package: "vapor"),
                .product(name: "Fluent", package: "fluent"),
                .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
                .product(name: "Redis", package: "redis"),
                .product(name: "JWT", package: "jwt"),
                .product(name: "Leaf", package: "leaf"),
            ]
        ),
        .testTarget(
            name: "AppTests",
            dependencies: [
                .target(name: "App"),
                .product(name: "XCTVapor", package: "vapor"),
            ]
        )
    ]
)

Application 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

import Vapor
import Fluent
import FluentPostgresDriver
import Redis
import JWT
import Leaf

func configure(_ app: Application) async throws {
    // MARK: - Environment

    let environment = app.environment

    // MARK: - Middleware

    app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
    app.middleware.use(ErrorMiddleware.default(environment: environment))
    app.middleware.use(CORSMiddleware())

    // MARK: - Database

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

    // MARK: - Migrations

    app.migrations.add(CreateUser())
    app.migrations.add(CreatePost())
    app.migrations.add(CreateUserToken())

    if environment == .development {
        try await app.autoMigrate()
    }

    // MARK: - Redis

    if let redisURL = Environment.get("REDIS_URL") {
        app.redis.configuration = try RedisConfiguration(url: redisURL)
    } else {
        app.redis.configuration = try RedisConfiguration(
            hostname: Environment.get("REDIS_HOST") ?? "localhost",
            port: Environment.get("REDIS_PORT").flatMap(Int.init) ?? 6379
        )
    }

    // MARK: - JWT

    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)

    // MARK: - Leaf

    app.views.use(.leaf)

    // MARK: - Routes

    try routes(app)
}

routes.swift

import Vapor

func routes(_ app: Application) throws {
    // Health check
    app.get("health") { req -> HTTPStatus in
        return .ok
    }

    // API routes
    let api = app.grouped("api", "v1")

    // Public routes
    try api.register(collection: AuthController())

    // Protected routes
    let protected = api.grouped(JWTAuthMiddleware())
    try protected.register(collection: UserController())
    try protected.register(collection: PostController())

    // Web routes
    app.get { req async throws -> View in
        return try await req.view.render("index", ["title": "Welcome"])
    }
}

Models with Fluent

User Model

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

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

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

    @Field(key: "is_active")
    var isActive: Bool

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

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

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

    @Children(for: \.$user)
    var tokens: [UserToken]

    init() {}

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

// MARK: - Role Enum

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

// MARK: - Authentication

extension User: ModelAuthenticatable {
    static let usernameKey = \User.$email
    static let passwordHashKey = \User.$passwordHash

    func verify(password: String) throws -> Bool {
        try Bcrypt.verify(password, created: self.passwordHash)
    }
}

// MARK: - Convenience Methods

extension User {
    static func create(
        email: String,
        password: String,
        name: String,
        role: Role = .user,
        on database: Database
    ) async throws -> User {
        let passwordHash = try Bcrypt.hash(password)
        let user = User(email: email, passwordHash: passwordHash, name: name, role: role)
        try await user.save(on: database)
        return user
    }

    func generateToken() throws -> UserToken {
        try UserToken(
            value: [UInt8].random(count: 32).base64,
            userID: self.requireID()
        )
    }
}

Post Model

import Fluent
import Vapor

final class Post: Model, Content, @unchecked Sendable {
    static let schema = "posts"

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

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

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

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

    @Enum(key: "status")
    var status: Status

    @Parent(key: "user_id")
    var user: User

    @Timestamp(key: "published_at", on: .none)
    var publishedAt: Date?

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

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

    init() {}

    init(
        id: UUID? = nil,
        title: String,
        content: String,
        slug: String,
        status: Status = .draft,
        userID: User.IDValue
    ) {
        self.id = id
        self.title = title
        self.content = content
        self.slug = slug
        self.status = status
        self.$user.id = userID
    }

    enum Status: String, Codable, CaseIterable {
        case draft
        case published
        case archived
    }
}

UserToken Model

import Fluent
import Vapor

final class UserToken: Model, Content, @unchecked Sendable {
    static let schema = "user_tokens"

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

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

    @Parent(key: "user_id")
    var user: User

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

    @Timestamp(key: "expires_at", on: .none)
    var expiresAt: Date?

    init() {}

    init(id: UUID? = nil, value: String, userID: User.IDValue) {
        self.id = id
        self.value = value
        self.$user.id = userID
        self.expiresAt = Date().addingTimeInterval(60 * 60 * 24 * 7) // 7 days
    }
}

extension UserToken: ModelTokenAuthenticatable {
    static let valueKey = \UserToken.$value
    static let userKey = \UserToken.$user

    var isValid: Bool {
        guard let expiresAt = expiresAt else { return true }
        return expiresAt > Date()
    }
}

Migrations

Create User Migration

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("name", .string, .required)
            .field("role", role, .required)
            .field("is_active", .bool, .required, .custom("DEFAULT true"))
            .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()
    }
}

Create Post Migration

import Fluent

struct CreatePost: AsyncMigration {
    func prepare(on database: Database) async throws {
        let status = try await database.enum("post_status")
            .case("draft")
            .case("published")
            .case("archived")
            .create()

        try await database.schema("posts")
            .id()
            .field("title", .string, .required)
            .field("content", .string, .required)
            .field("slug", .string, .required)
            .field("status", status, .required)
            .field("user_id", .uuid, .required, .references("users", "id", onDelete: .cascade))
            .field("published_at", .datetime)
            .field("created_at", .datetime)
            .field("updated_at", .datetime)
            .unique(on: "slug")
            .create()

        // Create index
        try await database.schema("posts")
            .index(on: "user_id")
            .update()
    }

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

DTOs (Data Transfer Objects)

User DTOs

import Vapor

// MARK: - Request DTOs

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 UpdateUserRequest: Content, Validatable {
    let name: String?
    let email: String?

    static func validations(_ validations: inout Validations) {
        validations.add("email", as: String?.self, is: .nil || .email)
        validations.add("name", as: String?.self, is: .nil || !.empty)
    }
}

struct LoginRequest: Content, Validatable {
    let email: String
    let password: String

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

// MARK: - Response DTOs

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

struct TokenResponse: Content {
    let accessToken: String
    let tokenType: String
    let expiresIn: Int

    init(token: String, expiresIn: Int = 3600) {
        self.accessToken = token
        self.tokenType = "Bearer"
        self.expiresIn = expiresIn
    }
}

struct PaginatedResponse<T: Content>: Content {
    let items: [T]
    let metadata: PageMetadata
}

struct PageMetadata: Content {
    let page: Int
    let perPage: Int
    let total: Int
    let totalPages: Int
}

Controllers

User Controller

import Vapor
import Fluent

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)

        // Current user routes
        users.get("me", use: me)
        users.put("me", use: updateMe)
    }

    // MARK: - Handlers

    /// GET /users
    @Sendable
    func index(req: Request) async throws -> PaginatedResponse<UserResponse> {
        let page = try req.query.decode(PageRequest.self)

        let users = try await User.query(on: req.db)
            .filter(\.$isActive == true)
            .sort(\.$createdAt, .descending)
            .paginate(PageRequest(page: page.page, per: page.per))

        let items = try users.items.map { try UserResponse(user: $0) }

        return PaginatedResponse(
            items: items,
            metadata: PageMetadata(
                page: page.page,
                perPage: page.per,
                total: users.metadata.total,
                totalPages: users.metadata.pageCount
            )
        )
    }

    /// GET /users/:userID
    @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)
    }

    /// PUT /users/:userID
    @Sendable
    func update(req: Request) async throws -> UserResponse {
        // Check admin permission
        let currentUser = try req.auth.require(User.self)
        guard currentUser.role == .admin else {
            throw Abort(.forbidden, reason: "Admin access required")
        }

        guard let user = try await User.find(req.parameters.get("userID"), on: req.db) else {
            throw Abort(.notFound, reason: "User not found")
        }

        try UpdateUserRequest.validate(content: req)
        let updateRequest = try req.content.decode(UpdateUserRequest.self)

        if let name = updateRequest.name {
            user.name = name
        }
        if let email = updateRequest.email {
            // Check email uniqueness
            if let existing = try await User.query(on: req.db)
                .filter(\.$email == email)
                .filter(\.$id != user.requireID())
                .first() {
                throw Abort(.conflict, reason: "Email already in use")
            }
            user.email = email
        }

        try await user.save(on: req.db)
        return try UserResponse(user: user)
    }

    /// DELETE /users/:userID
    @Sendable
    func delete(req: Request) async throws -> HTTPStatus {
        let currentUser = try req.auth.require(User.self)
        guard currentUser.role == .admin else {
            throw Abort(.forbidden, reason: "Admin access required")
        }

        guard let user = try await User.find(req.parameters.get("userID"), on: req.db) else {
            throw Abort(.notFound, reason: "User not found")
        }

        // Soft delete
        user.isActive = false
        try await user.save(on: req.db)

        return .noContent
    }

    /// GET /users/me
    @Sendable
    func me(req: Request) async throws -> UserResponse {
        let user = try req.auth.require(User.self)
        return try UserResponse(user: user)
    }

    /// PUT /users/me
    @Sendable
    func updateMe(req: Request) async throws -> UserResponse {
        let user = try req.auth.require(User.self)

        try UpdateUserRequest.validate(content: req)
        let updateRequest = try req.content.decode(UpdateUserRequest.self)

        if let name = updateRequest.name {
            user.name = name
        }
        if let email = updateRequest.email {
            if let existing = try await User.query(on: req.db)
                .filter(\.$email == email)
                .filter(\.$id != user.requireID())
                .first() {
                throw Abort(.conflict, reason: "Email already in use")
            }
            user.email = email
        }

        try await user.save(on: req.db)
        return try UserResponse(user: user)
    }
}

// Page request helper
struct PageRequest: Content {
    var page: Int
    var per: Int

    init(page: Int = 1, per: Int = 20) {
        self.page = max(1, page)
        self.per = min(100, max(1, per))
    }
}

Auth Controller

import Vapor
import Fluent

struct AuthController: RouteCollection {
    func boot(routes: RoutesBuilder) throws {
        let auth = routes.grouped("auth")

        auth.post("register", use: register)
        auth.post("login", use: login)

        // Protected routes
        let protected = auth.grouped(JWTAuthMiddleware())
        protected.post("logout", use: logout)
        protected.post("refresh", use: refresh)
    }

    // MARK: - Handlers

    /// POST /auth/register
    @Sendable
    func register(req: Request) async throws -> UserResponse {
        try CreateUserRequest.validate(content: req)
        let createRequest = try req.content.decode(CreateUserRequest.self)

        // Check if email already exists
        if let _ = try await User.query(on: req.db)
            .filter(\.$email == createRequest.email)
            .first() {
            throw Abort(.conflict, reason: "Email already registered")
        }

        let user = try await User.create(
            email: createRequest.email,
            password: createRequest.password,
            name: createRequest.name,
            on: req.db
        )

        return try UserResponse(user: user)
    }

    /// POST /auth/login
    @Sendable
    func login(req: Request) async throws -> TokenResponse {
        try LoginRequest.validate(content: req)
        let loginRequest = try req.content.decode(LoginRequest.self)

        guard let user = try await User.query(on: req.db)
            .filter(\.$email == loginRequest.email)
            .filter(\.$isActive == true)
            .first() else {
            throw Abort(.unauthorized, reason: "Invalid credentials")
        }

        guard try user.verify(password: loginRequest.password) else {
            throw Abort(.unauthorized, reason: "Invalid credentials")
        }

        // Generate JWT token
        let payload = UserPayload(
            subject: .init(value: try user.requireID().uuidString),
            expiration: .init(value: Date().addingTimeInterval(3600))
        )

        let token = try await req.jwt.sign(payload)

        return TokenResponse(token: token, expiresIn: 3600)
    }

    /// POST /auth/logout
    @Sendable
    func logout(req: Request) async throws -> HTTPStatus {
        // For JWT, logout is typically client-side
        // Optionally blacklist token in Redis
        let user = try req.auth.require(User.self)

        // Delete all tokens for user (if using database tokens)
        try await UserToken.query(on: req.db)
            .filter(\.$user.$id == user.requireID())
            .delete()

        return .noContent
    }

    /// POST /auth/refresh
    @Sendable
    func refresh(req: Request) async throws -> TokenResponse {
        let user = try req.auth.require(User.self)

        let payload = UserPayload(
            subject: .init(value: try user.requireID().uuidString),
            expiration: .init(value: Date().addingTimeInterval(3600))
        )

        let token = try await req.jwt.sign(payload)

        return TokenResponse(token: token, expiresIn: 3600)
    }
}

JWT Authentication

JWT Payload

import JWT
import Vapor

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

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

JWT Auth Middleware

import Vapor
import JWT

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

        do {
            // Verify and decode JWT
            let payload = try await request.jwt.verify(token, as: UserPayload.self)

            // Get user ID from payload
            guard let userID = UUID(payload.subject.value) else {
                throw Abort(.unauthorized, reason: "Invalid token payload")
            }

            // Load user from database
            guard let user = try await User.find(userID, on: request.db),
                  user.isActive else {
                throw Abort(.unauthorized, reason: "User not found or inactive")
            }

            // Authenticate request
            request.auth.login(user)

            return try await next.respond(to: request)
        } catch let error as JWTError {
            throw Abort(.unauthorized, reason: "Invalid token: \(error.localizedDescription)")
        }
    }
}

Error Handling

Custom 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 {
            return try await handleAbortError(abort, for: request)
        } catch let error as ValidationError {
            return try await handleValidationError(error, for: request)
        } catch {
            return try await handleUnknownError(error, for: request)
        }
    }

    private func handleAbortError(_ error: AbortError, for request: Request) async throws -> Response {
        let response = ErrorResponse(
            error: true,
            reason: error.reason,
            code: error.status.code
        )

        return try await response.encodeResponse(status: error.status, for: request)
    }

    private func handleValidationError(_ error: ValidationError, for request: Request) async throws -> Response {
        let response = ErrorResponse(
            error: true,
            reason: "Validation failed",
            code: 400,
            details: error.description
        )

        return try await response.encodeResponse(status: .badRequest, for: request)
    }

    private func handleUnknownError(_ error: Error, for request: Request) async throws -> Response {
        request.logger.error("Unexpected error: \(error)")

        let response = ErrorResponse(
            error: true,
            reason: request.application.environment.isRelease
                ? "An internal error occurred"
                : error.localizedDescription,
            code: 500
        )

        return try await response.encodeResponse(status: .internalServerError, for: request)
    }
}

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

    init(error: Bool, reason: String, code: UInt, details: String? = nil) {
        self.error = error
        self.reason = reason
        self.code = code
        self.details = details
    }
}

Services

User Service

import Vapor
import Fluent

protocol UserServiceProtocol: Sendable {
    func findByID(_ id: UUID, on db: Database) async throws -> User?
    func findByEmail(_ email: String, on db: Database) async throws -> User?
    func create(_ request: CreateUserRequest, on db: Database) async throws -> User
    func update(_ user: User, with request: UpdateUserRequest, on db: Database) async throws -> User
    func delete(_ user: User, on db: Database) async throws
}

struct UserService: UserServiceProtocol {
    func findByID(_ id: UUID, on db: Database) async throws -> User? {
        try await User.find(id, on: db)
    }

    func findByEmail(_ email: String, on db: Database) async throws -> User? {
        try await User.query(on: db)
            .filter(\.$email == email)
            .first()
    }

    func create(_ request: CreateUserRequest, on db: Database) async throws -> User {
        let passwordHash = try Bcrypt.hash(request.password)
        let user = User(
            email: request.email,
            passwordHash: passwordHash,
            name: request.name
        )
        try await user.save(on: db)
        return user
    }

    func update(_ user: User, with request: UpdateUserRequest, on db: Database) async throws -> User {
        if let name = request.name {
            user.name = name
        }
        if let email = request.email {
            user.email = email
        }
        try await user.save(on: db)
        return user
    }

    func delete(_ user: User, on db: Database) async throws {
        user.isActive = false
        try await user.save(on: db)
    }
}

// Register in configure.swift
extension Application {
    var userService: UserServiceProtocol {
        UserService()
    }
}

extension Request {
    var userService: UserServiceProtocol {
        application.userService
    }
}

Testing

Controller Tests

import XCTVapor
@testable import App

final class UserControllerTests: XCTestCase {
    var app: Application!

    override func setUp() async throws {
        app = try await Application.make(.testing)
        try await configure(app)
        try await app.autoMigrate()
    }

    override func tearDown() async throws {
        try await app.autoRevert()
        try await app.asyncShutdown()
        app = nil
    }

    func testRegisterUser() async throws {
        let createRequest = CreateUserRequest(
            email: "test@example.com",
            password: "password123",
            name: "Test User"
        )

        try await app.test(.POST, "api/v1/auth/register") { req in
            try req.content.encode(createRequest)
        } afterResponse: { res in
            XCTAssertEqual(res.status, .ok)

            let user = try res.content.decode(UserResponse.self)
            XCTAssertEqual(user.email, "test@example.com")
            XCTAssertEqual(user.name, "Test User")
        }
    }

    func testLoginUser() async throws {
        // Create user first
        let user = try await User.create(
            email: "login@example.com",
            password: "password123",
            name: "Login User",
            on: app.db
        )

        let loginRequest = LoginRequest(
            email: "login@example.com",
            password: "password123"
        )

        try await app.test(.POST, "api/v1/auth/login") { req in
            try req.content.encode(loginRequest)
        } afterResponse: { res in
            XCTAssertEqual(res.status, .ok)

            let token = try res.content.decode(TokenResponse.self)
            XCTAssertFalse(token.accessToken.isEmpty)
            XCTAssertEqual(token.tokenType, "Bearer")
        }
    }

    func testGetUsersRequiresAuth() async throws {
        try await app.test(.GET, "api/v1/users") { res in
            XCTAssertEqual(res.status, .unauthorized)
        }
    }

    func testGetUsersWithAuth() async throws {
        // Create user and get token
        let user = try await User.create(
            email: "auth@example.com",
            password: "password123",
            name: "Auth User",
            on: app.db
        )

        let payload = UserPayload(
            subject: .init(value: try user.requireID().uuidString),
            expiration: .init(value: Date().addingTimeInterval(3600))
        )
        let token = try await app.jwt.keys.sign(payload)

        try await app.test(.GET, "api/v1/users") { req in
            req.headers.bearerAuthorization = BearerAuthorization(token: token)
        } afterResponse: { res in
            XCTAssertEqual(res.status, .ok)

            let response = try res.content.decode(PaginatedResponse<UserResponse>.self)
            XCTAssertFalse(response.items.isEmpty)
        }
    }
}

Docker Configuration

Dockerfile

# Build stage
FROM swift:5.9-jammy as build

WORKDIR /app

# Copy dependencies first for caching
COPY Package.swift Package.resolved ./
RUN swift package resolve

# Copy source and build
COPY . .
RUN swift build -c release --static-swift-stdlib

# Production stage
FROM ubuntu:jammy

RUN apt-get update && apt-get install -y \
    libcurl4 \
    libxml2 \
    tzdata \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY --from=build /app/.build/release/App /app/App
COPY --from=build /app/Public /app/Public
COPY --from=build /app/Resources /app/Resources

ENV ENVIRONMENT=production

EXPOSE 8080

ENTRYPOINT ["./App"]
CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"]

docker-compose.yml

version: "3.8"

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - DATABASE_URL=postgres://vapor:vapor@db:5432/vapor
      - REDIS_URL=redis://redis:6379
      - JWT_SECRET=${JWT_SECRET}
    depends_on:
      - db
      - redis

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: vapor
      POSTGRES_PASSWORD: vapor
      POSTGRES_DB: vapor
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  postgres_data:

Best Practices

Performance

  • ✓ Use async/await throughout
  • ✓ Implement pagination for list endpoints
  • ✓ Use eager loading to prevent N+1 queries
  • ✓ Cache frequently accessed data in Redis
  • ✓ Use database indexes for query optimization

Security

  • ✓ Always validate input with Validatable
  • ✓ Use Bcrypt for password hashing
  • ✓ Implement rate limiting
  • ✓ Use HTTPS in production
  • ✓ Sanitize user input
  • ✓ Use parameterized queries (Fluent handles this)

Architecture

  • ✓ Use DTOs for request/response
  • ✓ Keep controllers thin
  • ✓ Extract business logic to services
  • ✓ Use dependency injection
  • ✓ Write comprehensive tests

References