Vapor Best Practices
1. Concurrency
- ALWAYS use Swift Concurrency (
async/await) overEventLoopFuture. - All route handlers MUST be annotated with
@Sendable.
2. Controllers & Routing
- Organize routes into
RouteCollectionconformances. - Do not put logic in
routes.swift; delegate immediately to a Controller. - Group routes by feature (e.g.,
UsersController,AuthController). - Register controllers:
app.register(collection: MyController()). - API versioning:
app.grouped("api", "v1").
3. Middleware
- Use
AsyncMiddlewarefor custom middleware:
struct AuthMiddleware: AsyncMiddleware {
func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response {
let payload = try await request.jwt.verify(as: UserJWTPayload.self)
request.auth.login(payload)
return try await next.respond(to: request)
}
}- Apply to route groups:
routes.grouped(AuthMiddleware()).
4. Fluent (Database)
- Use
@Parentand@Childrenproperty wrappers correctly. - Always use DTOs (Data Transfer Objects) implementing
Contentfor API requests/responses. NEVER return a Fluent Model directly to the client. - Run migrations via
app.migrations.add(...). - Filter with
$syntax:.filter(\.$deviceID == id).
Direct SQL Access (for complex queries not expressible via Fluent):
guard let sql = req.db as? any SQLDatabase else {
throw Abort(.internalServerError)
}
let rows = try await sql.raw("SELECT ... FROM \(raw: Model.schema)").all(decoding: SomeRow.self)5. Environment
- Use
Environment.get("KEY")for configuration. - Support
ProductionvsDevelopmentmodes explicitly inconfigure.swift.
Example Route
struct UsersController: RouteCollection {
func boot(routes: RoutesBuilder) throws {
let users = routes.grouped("users")
users.get(use: index)
}
@Sendable
func index(req: Request) async throws -> [UserDTO] {
let users = try await User.query(on: req.db).all()
return users.map { $0.toDTO() }
}
}