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", from: "0.1.0")
]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 VaporKitAttach @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 user.save(on: req.db)
return .created
}
}The route closure can use any request parameter name, or shorthand $0.
Explicit return types are preserved, so declarations such as
{ req -> HTTPStatus in ... } generate a handler returning HTTPStatus.
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 #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 only one expression should be skipped.
let value = #Bypass {
try req.parameters.require(dynamicName)
}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