HTTP 请求的结构
每一个到达 Vapor 服务器的 HTTP 请求都被封装成 Request 对象。理解 Request 的内部结构,是编写正确处理逻辑的前提。
HTTP 请求的组成部分:
POST /api/v1/users HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbG...
X-Request-ID: abc-123
{
"name": "Alice",
"email": "alice@example.com",
"password": "secret123"
}
在 Vapor Request 对象中的对应关系:
┌─────────────────────────────────────────┐
│ req.method → .POST │
│ req.url.path → /api/v1/users │
│ req.headers["Content-Type"] │
│ → application/json │
│ req.headers.bearerAuthorization?.token │
│ → eyJhbG... │
│ req.content.decode(UserDTO.self) │
│ → UserDTO 对象 │
│ req.parameters.get("id") │
│ → 路径参数 │
│ req.query[Int.self, at: "page"] │
│ → 查询参数 │
└─────────────────────────────────────────┘
核心概念词典
Content 协议
Vapor 的内容序列化/反序列化协议,任何实现了
Content(同时继承自 Codable)的类型都可以直接通过 req.content.decode() 从请求体解码,或作为路由处理函数的返回值自动编码为响应体。Decodable / Encodable
Swift 标准库协议,
Codable = Decodable & Encodable。只要结构体/类实现这两个协议,Swift 编译器会自动合成 JSON 序列化代码,无需手写。Vapor 的 Content 协议基于此扩展,支持 JSON、URL-encoded form、multipart 多种内容类型。Abort / AbortError
Abort 是 Vapor 的内置错误类型,构造时传入 HTTP 状态码和可选的错误原因。在路由处理函数中 throw Abort(.notFound, reason: "User not found") 会自动生成对应的 HTTP 错误响应(状态码 + JSON 错误体),是统一错误处理的核心工具。HTTPStatus
HTTP 状态码的类型安全枚举,覆盖所有标准状态码:
.ok(200)、.created(201)、.noContent(204)、.badRequest(400)、.unauthorized(401)、.notFound(404)、.internalServerError(500)等。Response
HTTP 响应对象,包含状态码、响应头和响应体。路由处理函数可以直接返回
Response 对象来精确控制响应内容,也可以返回任何 AsyncResponseEncodable 类型由 Vapor 自动转换。请求体解码:DTO 模式
DTO(Data Transfer Object,数据传输对象)是服务端开发的重要设计模式:用单独的结构体表示 API 的输入输出格式,与数据库 Model 分离。这样可以避免 API 直接暴露数据库字段(如 passwordHash),也方便对输入做验证和转换。
// 输入 DTO:客户端发来的数据
struct CreateUserDTO: Content {
let name: String
let email: String
let password: String
// 自定义验证
func validate() throws {
guard email.contains("@") else {
throw Abort(.badRequest, reason: "Invalid email format")
}
guard password.count >= 8 else {
throw Abort(.badRequest, reason: "Password must be at least 8 characters")
}
}
}
// 输出 DTO:返回给客户端的数据(不含 passwordHash!)
struct UserResponse: Content {
let id: UUID
let name: String
let email: String
let createdAt: Date?
init(from user: User) {
self.id = user.id!
self.name = user.name
self.email = user.email
self.createdAt = user.createdAt
}
}
// 路由处理函数
app.post("users") { req async throws -> Response in
// 1. 解码请求体(失败自动返回 400)
let dto = try req.content.decode(CreateUserDTO.self)
// 2. 业务验证
try dto.validate()
// 3. 检查邮箱是否已存在
if try await User.query(on: req.db).filter(\.$email == dto.email).first() != nil {
throw Abort(.conflict, reason: "Email already in use")
}
// 4. 创建 Model(密码哈希处理)
let hash = try Bcrypt.hash(dto.password)
let user = User(name: dto.name, email: dto.email, passwordHash: hash)
try await user.save(on: req.db)
// 5. 返回 201 Created + 资源 URL
let response = UserResponse(from: user)
return try req.response(.created, content: response)
}
DTO 与 Model 分离是好习惯
永远不要直接将数据库 Model 作为 API 的输入输出。输入 DTO 做验证和清洗,输出 DTO 控制暴露字段,这是安全和可维护性的基础。当数据库结构变更时,只需调整 DTO 的转换逻辑,不影响 API 契约。
统一错误响应
Vapor 的 ErrorMiddleware(默认已注册)会捕获所有从路由函数中抛出的错误,将其转换为统一的 JSON 错误响应。你也可以实现 AbortError 协议来定制错误格式:
// 自定义错误类型
enum AppError: AbortError {
case userNotFound(UUID)
case emailAlreadyExists(String)
case insufficientPermissions
var status: HTTPResponseStatus {
switch self {
case .userNotFound: return .notFound
case .emailAlreadyExists: return .conflict
case .insufficientPermissions: return .forbidden
}
}
var reason: String {
switch self {
case .userNotFound(let id): return "User \(id) not found"
case .emailAlreadyExists(let email): return "Email '\(email)' is already registered"
case .insufficientPermissions: return "You don't have permission to perform this action"
}
}
}
// 使用自定义错误
app.get("users", ":id") { req async throws -> UserResponse in
let id = try req.parameters.require("id", as: UUID.self)
guard let user = try await User.find(id, on: req.db) else {
throw AppError.userNotFound(id)
}
return UserResponse(from: user)
}
// 客户端收到的响应:
// HTTP/1.1 404 Not Found
// {"error": true, "reason": "User abc-123 not found"}
文件上传处理
文件上传使用 multipart/form-data 内容类型。Vapor 内置了 multipart 解析支持,通过 File 类型接收上传的文件数据:
struct UploadDTO: Content {
var file: File // 文件字段
var description: String? // 额外文本字段
}
app.post("upload") { req async throws -> Response in
// 限制上传大小(默认 16KB body size,需要调大)
app.routes.defaultMaxBodySize = "10mb"
let upload = try req.content.decode(UploadDTO.self)
let file = upload.file
// 验证文件类型
guard let ext = file.extension, ["jpg", "png", "gif"].contains(ext) else {
throw Abort(.badRequest, reason: "Only image files are allowed")
}
// 保存文件到磁盘
let filename = "\(UUID()).\(ext)"
let uploadPath = req.application.directory.publicDirectory + "uploads/" + filename
try await req.fileio.writeFile(ByteBuffer(buffer: file.data), at: uploadPath)
return Response(status: .created, headers: ["Location": "/uploads/\(filename)"])
}
文件上传:默认请求体大小限制只有 16KB
Vapor 默认限制请求体为 16KB,上传文件时必须显式调大:
app.routes.defaultMaxBodySize = "10mb" 或在具体路由上用 .body(.collect(maxSize: "50mb"))。超过限制的请求会返回 413 Payload Too Large。注意:app.routes.defaultMaxBodySize 的赋值应在 configure.swift 中完成,而不是在路由处理函数内部(函数内部每次请求都会重新赋值,虽然有效但不优雅)。
Vapor Validation:内置验证框架
除了手动在 DTO 里写 validate() 方法,Vapor 提供了内置的 Validatable 协议,通过声明式方式定义验证规则,错误信息自动聚合:
import Vapor
struct CreateUserDTO: Content, Validatable {
let name: String
let email: String
let password: String
let age: Int
// 声明验证规则(Validatable 协议要求实现此静态方法)
static func validations(_ validations: inout Validations) {
validations.add("name", as: String.self, is: .count(2...50))
validations.add("email", as: String.self, is: .email)
validations.add("password", as: String.self, is: .count(8...) & .ascii)
validations.add("age", as: Int.self, is: .range(18...))
}
}
// 路由中调用 validate() — 失败自动抛出包含所有错误的 400 响应
app.post("users") { req async throws -> UserResponse in
try CreateUserDTO.validate(content: req) // 一行验证全部规则
let dto = try req.content.decode(CreateUserDTO.self)
// 验证通过后才会执行到这里
...
}
// 客户端收到的验证失败响应示例:
// HTTP/1.1 400 Bad Request
// {
// "error": true,
// "reason": "email is not a valid email address, password is less than minimum of 8 character(s)"
// }
内置验证器(Validator)
Vapor 提供了丰富的内置验证器:
.email(邮箱格式)、.count(n...m)(字符数/元素数范围)、.range(n...)(数值范围)、.ascii(ASCII 字符)、.alphanumeric(字母数字)、.url(URL 格式)、.nil(允许为空)。多个验证器可以用 &(且)和 ||(或)组合。自定义验证器
实现
ValidatorResults 和 Validator 协议可以创建自定义验证逻辑,例如验证中国手机号格式、验证业务规则(如用户名不允许包含特定词汇)。自定义验证器可以与内置验证器通过 & 组合使用。Content 协议与 Validatable
Content 协议(继承自 Codable)负责序列化;Validatable 协议负责业务验证。两者通常配合使用——先用 Content 解码,再用 Validatable 验证。如果解码失败(字段缺失、类型错误),Vapor 自动返回 400,不会进入验证阶段。分页响应模式
列表 API 通常需要分页。Vapor 内置了 Page 类型支持分页查询,与 Fluent 的 .paginate() 配合使用非常方便:
// GET /users?page=1&per=20
app.get("users") { req async throws -> Page<UserResponse> in
// Fluent 自动从查询参数读取 page 和 per,生成 LIMIT/OFFSET SQL
let page = try await User.query(on: req.db)
.sort(\.$createdAt, .descending)
.paginate(for: req)
// 将 Page<User> 映射为 Page<UserResponse>
return page.map { UserResponse(from: $0) }
}
// 客户端收到的响应结构:
// {
// "items": [...],
// "metadata": {
// "page": 1,
// "per": 20,
// "total": 143
// }
// }
永远不要在生产环境暴露 errorIdentifier 和内部错误详情
Vapor 的默认 ErrorMiddleware 在
development 环境下会在错误响应中包含详细的错误描述(堆栈信息等),但在 production 环境自动隐藏。确保部署时设置正确的环境变量:APP_ENV=production 或启动参数 --env production。内部错误信息泄露(如数据库查询失败的 SQL)是严重的安全风险。此外,密码相关字段(password、passwordHash、token)永远不应出现在任何 DTO 的输出结构(UserResponse)中。
本章小结
Content 协议统一了 JSON、form 数据和 multipart 的编解码;DTO 模式将 API 层与数据库层解耦,防止敏感字段泄露;Abort 和 AbortError 提供了优雅的错误传播机制;Vapor 内置的 Validatable 框架通过声明式规则(
.email、.count()、.range())简化验证逻辑,失败时自动聚合所有错误信息返回 400;.paginate(for: req) 与 Fluent 无缝集成,自动处理 LIMIT/OFFSET 分页。这些工具共同构成 Vapor API 层的完整工具链,下一章将在此基础上加入 JWT 认证。