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)"])
}
本章小结
Content 协议统一了 JSON、form 数据和 multipart 的编解码;DTO 模式将 API 层与数据库层解耦;Abort 和 AbortError 提供了优雅的错误传播机制。这三者共同构成了 Vapor API 层的完整工具链。下一章将在此基础上加入认证与 JWT,让 API 具备身份验证能力。