VaporKit is a Swift macro package for reducing repetitive Vapor routing and validation code while keeping the generated code close to Vapor's native APIs.
It focuses on two workflows:
- Building
RouteCollectionimplementations with route declaration macros. - Building
Validatablemodels with property-level validation constraints.
- Swift 6.3 or newer
- Vapor 4.121.0 or newer
- macOS, iOS, tvOS, watchOS, or Mac Catalyst targets supported by the package
Add VaporKit to your package dependencies.
dependencies: [
.package(url: "https://github.com/AreroKetahi/vaporkit.git", branch: "main")
]Then add the library product to your target.
.target(
name: "App",
dependencies: [
.product(name: "VaporKit", package: "vaporkit")
]
)Import it where you define routes or validation models.
import Vapor
import VaporKitVaporKit supports two routing declaration styles:
- Convenient routing declarations use compact route closures.
- Parameterized routing declarations use regular functions with typed parameters in the signature.
Attach @Router to a type to synthesize RouteCollection conformance and a
boot(routes:) implementation.
@Router("api/users")
struct UserRoutes {
#Get(":id") { req in
let id = try req.parameters.require("id", as: UUID.self)
return "User \(id)"
}
#Post { req -> HTTPStatus in
let user = try req.content.decode(CreateUserRequest.self)
try await createUser(user, on: req.db)
return .created
}
}Use this style for short handlers that are easiest to read inline. The closure
can use any request parameter name, or shorthand $0.
VaporKit provides method-specific helpers:
#Get#Post#Put#Delete
Use #On when you need to provide the method explicitly.
#On("trace/:id", method: .TRACE) { req in
try req.parameters.require("id")
}Use @RouteHandler when you want to keep a named function and register it from
the generated boot(routes:).
@Router("api")
struct AdminRoutes {
@RouteHandler("health", method: .GET)
func health(req: Request) -> HTTPStatus {
.ok
}
}Attach @Middleware to a route declaration or route handler function.
@Middleware(AuthMiddleware(), RateLimitMiddleware())
#Get("profile") { req in
req.url.path
}The generated route is registered on routes.grouped(...).
Use #Register to register one or more child RouteCollection values below
the enclosing router prefix.
@Router("api/:tenantID")
struct APIRoutes {
#Register(UserRoutes(), AdminRoutes())
}Use this style when a handler has named inputs or is clearer as a regular
function. The first parameter is Request; additional values are declared with
@Path, @Query, @ContentBody, or @Auth.
struct SearchQuery: Decodable {
var term: String
var limit: Int
}
struct UpdateUserRequest: Content {
var username: String
}
@Router("api/users")
struct UserRoutes {
@Get(":id")
func show(
req: Request,
@Path id: UUID,
@Query("include.profile") includeProfile: Bool = false
) async throws -> UserDTO {
try await loadUser(id, includeProfile: includeProfile, on: req.db)
}
@Get("search")
func search(
req: Request,
@Query input: SearchQuery,
@Query("filter.name") name: String?,
@Query("page/number") page: Int = 1
) async throws -> [UserDTO] {
try await searchUsers(input, name: name, page: page, on: req.db)
}
@Put(":id")
func update(
req: Request,
@Path id: UUID,
@ContentBody body: UpdateUserRequest
) async throws -> UserDTO {
try await updateUser(id, with: body, on: req.db)
}
@Get("profile")
func profile(req: Request, @Auth user: User) async throws -> UserDTO {
try await loadProfile(for: user, on: req.db)
}
}Use @Path for route parameters, @Query for query values, and
@ContentBody for request bodies. Use @Auth for values loaded from
Request.auth. @Query, @ContentBody, and @Auth support optional types
and default values.
Use #WebSocket inside a router and declare events with #OnText, #OnBinary,
and #OnClose.
@Router("api")
struct SocketRoutes {
#WebSocket("chat") { req in
["X-Request-ID": req.id.uuidString]
} didUpgrade: {
#OnText {
await $0.send($1)
}
#OnBinary { ws, buffer in
await ws.send(buffer)
}
#OnClose {
print("closed")
}
}
}#OnText and #OnBinary support either explicit parameters or $0 / $1.
When shorthand is used, VaporKit rewrites those references to generated unique
names to avoid collisions with user code.
VaporKit performs syntax-only checks for direct route parameter access:
req.parameters.get("id")
try req.parameters.require("id")If a handler reads a parameter that is not declared in the route path, the macro emits a diagnostic.
#Get("users/:id") { req in
try req.parameters.require("slug") // compile-time diagnostic
}When a child router receives parameters from a parent router, declare those
names with #ForwardParameters.
@Router("users")
struct UserRoutes {
#ForwardParameters("tenantID")
#Get(":id") { req in
let tenantID = try req.parameters.require("tenantID")
let id = try req.parameters.require("id")
return "\(tenantID)/\(id)"
}
}Use @DisableParameterCheck to opt out at the router or route level.
@DisableParameterCheck
@Router("legacy")
struct LegacyRoutes {
#Get("dynamic") { req in
try req.parameters.require("runtimeOnly")
}
}
Use #Bypass when one expression or local code block should be skipped.
let value = #Bypass {
let fallback = try req.parameters.require("id")
return req.parameters.get(dynamicName) ?? fallback
}The checker is intentionally syntax-only. It does not resolve aliases, type information, or every possible expression form.
Attach @ValidatableModel to a model and describe property validations with
@Constraint.
@ValidatableModel
struct CreateUserRequest: Content {
@Constraint(.alphanumeric && .count(3...32))
var username: String
@Constraint(.email)
var email: String
@Constraint(.count(8...), message: "Password is too short.")
var password: String
}VaporKit generates the Validatable conformance and validations(_:)
implementation.
ValidationRule supports common Vapor validators:
.ascii.alphanumeric.email.empty.url.nil.characterSet(...).count(...).range(...).in(...)
Rules can be composed with !, &&, and ||.
@Constraint(.email || .empty)
var recoveryEmail: StringUse the custom constraint overload for predicates that cannot be expressed by built-in validators.
@Constraint(validating: String.self, message: "Name is reserved.") { name in
!["admin", "root", "system"].contains(name.lowercased())
}
var username: StringThe DocC catalog includes task-oriented guides:
Create RouterMigrating Code From Vapor-style RoutingBuild a Validation SystemMigrating From Vapor Validation
Generate documentation with Swift Package Manager or view it in Xcode's documentation browser.
Run the test suite with:
swift testIn constrained environments, it can be useful to redirect Swift and Clang module caches:
env CLANG_MODULE_CACHE_PATH=/tmp/clang-module-cache \
SWIFTPM_MODULECACHE_OVERRIDE=/tmp/swiftpm-module-cache \
swift test