RoutingTable
is a declarative routing library for use with the Vapor server-side Swift web framework. RoutingTable
allows you to set up routes like this:
import Vapor
import RoutingTable
public func routes(_ router: Router) throws {
let table = RoutingTable(
.scope("api", middleware: [ApiKeyMiddleware()], children: [
.resource("users", parameter: User.self, using: UserController.self, children: [
.resource("sprockets", parameter: Sprocket.self, using: SprocketController.self),
.resource("widgets", using: WidgetController.self)
]),
.resource("sessions", using: SessionController.self),
.post("do_stuff", using: StuffController.doStuff)
])
)
table.register(routes: router)
}
RoutingTable
defines the routes with high level concepts like scopes
and resources
, and structures them hierarchically.
RoutingTable
is installed via the Swift Package Manager. To add it to your project, modify the Package.swift
file to include it as a dependency:
// ...snip...
let package = Package(
// ...snip...
dependencies: [
// ...snip...
// Add this line:
.package(url: "https://github.com/andyfinnell/VaporRoutingTable.git", from: "0.0.1")
],
targets: [
.target(name: "App", dependencies: [
// ...snip...
// Add this line:
"RoutingTable"
]),
// ...snip...
]
)
The RoutingTable
library layers on top of the Vapor
framework to make it easier to declare and maintain routes. Most of the information needed to set up the routes RoutingTable
can infer based on Swift types, but there are couple of configuration steps that need to be taken before RoutingTable
can be used.
First, RoutingTable
needs to be able to create controllers at runtime so they can handle their assigned routes. Vapor's Services
dependency injection framework is leveraged to do this. As a result, all controllers need to conform to the Service
protocol and be registered with the dependency injection framework.
One straight forward way to accomplish this is to conform the controllers to ServiceType
:
final class MyAPIController {
// ...snip...
}
extension MyAPIController: ServiceType {
static func makeService(for worker: Container) throws -> MyAPIController {
return MyAPIController()
}
}
And then in configure()
register it:
services.register(MyAPIController.self)
The second part of configuration is modifying the routes.swift
file to use RoutingTable
:
import Vapor
import RoutingTable
public func routes(_ router: Router) throws {
let table = RoutingTable(
// Your routes go here
)
table.register(routes: router)
}
There are two steps to be done. First, declaring all your routes using RoutingTable
(see below for how to do that). And second, registering all of those declared routes onto Vapor's Router
using RoutingTable.register(routes:)
.
There a three kinds of routes that can be declared using RoutingTable
: scopes, resources, and raw endpoints.
A scope is useful for grouping routes together under a common path prefix and/or middleware. For example, it can be used to group API functionality or admin functionality together.
Using the API example:
let table = RoutingTable(
.scope("api", middleware: [ApiKeyMiddleware()], children: [
// Put any API resources or endpoints here
])
)
All routes under the scope
declared here will be prefixed with the "api" path and be processed by the ApiKeyMiddleware
.
Scopes can be nested and can overlap each other's path prefixes, as long as they don't declare the same endpoints. For example:
let table = RoutingTable(
.scope("api", middleware: [ApiKeyMiddleware()], children: [
.post("register", using: RegisterController.register)
]),
.scope("api", middleware: [AuthenticationMiddleware()], children: [
.post("change_password", using: PasswordController.update)
]),
)
Since the endpoints declared above are different, the routing table is valid.
RoutingTable
allows REST-like resources to be modeled directly in the routes. Resources can be nested, and can support any of the following operations: index, show, create, update, delete, new, and edit. When a resource is declared, RoutingTable
will inspect the type and determine which of the operations is supported by the controller, and automatically register those routes.
let table = RoutingTable(
.resource("users", parameter: User.self, using: UserController.self, children: [])
)
This requires some type support from the controller, in the form of conforming to the correct protocol. For example, if a controller supports the show operation, it must conform to the ResourceShowable
protocol:
extension UserController: ResourceShowable {
func show(_ request: Request) throws -> Future<Response> {
// logic for showing here
}
}
The show(_:)
method above will be registered as a handler for the GET /users/:user_id
route for this example.
This table shows each operation, the required protocol, and the routes the operation registers:
Operation | Protocol | Routes |
---|---|---|
index | ResourceIndexable |
GET /users |
show | ResourceShowable |
GET /users/:user_id |
create | ResourceCreatable |
POST /users |
update | ResourceUpdatable |
PUT /users/:user_id and PATCH /users/:user_id |
delete | ResourceDeletable |
DELETE /users/:user_id |
new | ResourceNewable |
GET /users/new |
edit | ResourceEditable |
GET /users/:user_id/edit |
RoutingTable
allows you to select which of the operations supported by a controller are registered. This can be useful in the case where a resource is declared at two different locations and should support different operations in each. An example of this could be a normal user's API vs. an admin user's API. The resource()
method accepts the only
and except
parameters to restrict the operations to a subset of the controller's supported operations.
For example this would only register routes for the create and new operations:
let table = RoutingTable(
.resource("users", parameter: User.self, using: UserController.self, only: [.create, .new], children: [])
)
This example would register routes for all supported operations, except delete:
let table = RoutingTable(
.resource("users", parameter: User.self, using: UserController.self, except: [.delete], children: [])
)
RoutingTable
also allows you to declare HTTP endpoints directly, which is useful for endpoints that are not REST-like. All the main HTTP methods are available: GET, PUT, PATCH, POST, DELETE. Simply declare the method and path you want handled, and RoutingTable
with infer the parameters, if any, from the provided handler.
public func routes(_ router: Router) throws {
let table = RoutingTable(
.post("do_stuff", using: StuffController.doStuff)
)
table.register(routes: router)
}
final class StuffController {
struct Parameters: Content {
let parameter1: Bool
let parameter2: String
}
func doStuff(_ request: Request, parameters: Parameters) throws -> Future<MyResponse> {
// ...your logic goes here...
}
}