Router is a highly extensibly and customizable HTTP router.
import PackageDescription
let package = Package(
name: "HelloZewo"
dependencies: [
.Package(url: "https://github.com/Zewo/Router.git", majorVersion: 0, minor: 7),
]
)
A simple 'hello world' example looks like so:
import Router
let router = Router { route in
route.get("/hello") { request in
return Response(body: "Hello, Zewo!")
}
}
The router can then be passed to anything that takes an S4-compatible Responder
. A Responder
is just something that takes a Request
and returns a Response
. Router
's role as a Responder
is to take that Request
and pass it along to the route that matches the path.
The most common use of a route would be to handle requests from an HTTPServer. Using the HTTPServer
module, for example, we can do just that:
let server = Server(responder: router)
server.start()
If you were to execute the above code and go to localhost:8080/hello
in your browser, you would see a plaintext response of "Hello, Zewo!".
Often times, you want your paths to be dynamic so that you can embed information without using query parameters. With Router
, any route component that starts with a colon (:
) is considered a parameter.
A parameter component will match any string and let it take its place. That is, the route /hello/:object
will match /hello/world
and /hello/123
. However, it will not match /hello
or /hello/world/123
.
In your route handler, you can extract the value of the path parameter through the pathParameters
property on Request
like so:
route.get("/hello/:object") { request in
guard let object = request.pathParameters["object"] else {
return Response(status: .internalServerError)
}
return Response(body: "Hello, \(object)!")
}
The above route will not only respond to /hello/world
with "Hello, world!"
, but also to /hello/there
with "Hello, there!"
and to /hello/123
with "Hello, 123!"
(and so on).
There is no limit to how many path parameters can be in a url. For example, the following route, which is defined as /:greeting/:location
, will respond to /hey/there
with "hey, there!"
as expected.
route.get("/:greeting/:location") { request in
guard let
greeting = request.pathParameters["greeting"],
location = request.pathParameters["location"]
else {
return Response(status: .internalServerError)
}
return Response(body: "\(greeting), \(location)!")
}
You can also use a wildcard (*
) in your routes, which matches all paths beginning with the given path parameters preceding the wildcard. For example, /static/*
will match /static/script.js
and /static/scripts/script.js
and so on, but not just /static
.
In case of conflicting routes, the default matcher (TrieRouteMatcher) will rank them based on the following order of priority:
- static
- parameter
- wildcard
That way, with routes /hello/world
, /hello/:greeting
, and /hello/*
, /hello/world
would get matched by only the static route and not by the parameter or wildcard routes.
Middleware is a powerful part of the Zewo application structure. Router
, along with all Responder
s, can have middleware applied to them. We will create a middleware which will catch errors in the responder chain and respond with a customizable error page. Zewo already provides this for you by the name of Recovery Middleware, but as an exercise we will re-implement it anyway.
This is the ideal syntax that we want to end up with:
enum CustomError: ErrorProtocol {
case badId
}
let recovery = RecoveryMiddleware { error in
switch error {
case CustomError.badId:
return Response(body: "The id doesn't exist!")
default:
throw error // dont handle, rethrow it
}
}
let router = Router(middleware: recovery) { route in
route.get("/:id") { route in
guard let
id = request.pathParameters["id"]
where id != "-1"
else {
throw CustomError.badId
}
// do something with id...
return Response(...)
}
}
The definition of middleware actually comes from Open Swift, which is a standard organization that Zewo and several other server-side-swift players have started. The Middleware
protocol:
public protocol Middleware {
func respond(to request: Request, chainingTo next: Responder) throws -> Response
}
So, here is the starting boilerplate for our middleware.
struct RecoveryMiddleware {
let recover: (ErrorProtocol) throws -> Response
public func respond(to request: Request, chainingTo next: Responder) throws -> Response {
// middleware code goes here
}
}
Let's study the required respond
method. We are passed in the request and the next responder in the responder chain. This gives us a lot of flexibility: we can modify the request before we pass it to the responder, and modify the response that the responder generates. We can also catch any errors that the responder may throw:
func respond(to request: Request, chainingTo next: Responder) throws -> Response {
// we don't modify the request beforehand
do {
// return the next responder, catch possible errors
return try next.respond(to: request)
} catch {
// pass the error to the `recover` method
return try self.recover(error)
}
}
That's it! It's very simple and powerful. We have now achieved the behavior that was showcased in the first code block.
A router can quickly become huge or have a lot of repetitive routes. To address this issue, Router
comes with the ability to compose routers together.
Take this router, for example:
let router = Router { route in
// prefixed with /v1/
route.get("/api/v1") { ... }
route.get("/api/v1/thing") { ... }
route.get("/api/v1/thing/:id") { ... }
route.put("/api/v1/thing/:id") { ... }
// prefixed with /v2/
route.get("/api/v2") { ... }
route.get("/api/v2/object") { ... }
route.get("/api/v2/object/:id") { ... }
route.put("/api/v2/object/:id") { ... }
}
There is a lot of repetition going on there. Not only are we prefixing each route with /api/v1
or /api/v2
, but we are also repeating /thing
and /object
. This is exactly the kind of situation where you should use route.compose
. What we're going to do is extract the /api
, /v1
, and /v2
routers and compose them all together into one big router.
Let's start from top. Our new base router is going to look something like this:
let mainRouter = Router { route in
route.compose("/api", router: apiRouter)
}
With the route.compose
call, the mainRouter
is now going to be forwarding all of its requests that start with /api
to apiRouter
.
apiRouter
is essentially the same thing.
let apiRouter = Router { route in
route.compose("/v1", router: v1Router)
route.compose("/v2", router: v2Router)
}
v1Router
and v2Router
are where the bulk of the routes are going to be. Notice how the routers are totally encapsulated and have no knowledge of how they're going to be embedded in other routers.
let v1Router = Router { route
route.get("/") { ... }
route.get("/thing") { ... }
route.get("/thing/:id") { ... }
route.put("/thing/:id") { ... }
}
let v2Router = Router { route in
route.get("/") { ... }
route.get("/object") { ... }
route.get("/object/:id") { ... }
route.put("/object/:id") { ... }
}
For the sake of the example, lets assume that thing
and object
are identical, and the only difference between them is that their name changed from version 1 to version 2. To avoid that code duplication, lets create a new objectRouter
that both v1Router
and v2Router
can use under different paths.
let objectRouter = Router { route in
route.get("/") { ... }
route.get("/:id") { ... }
route.put("/:id") { ... }
}
Wonderful! Now, v1Router
and v2Router
can both use objectRouter
and get rid of the unnecessary code duplication.
let v1Router = Router { route in
route.get("/") { ... }
route.compose("/thing", router: objectRouter)
}
let v2Router = Router { route in
route.get("/") { ... }
route.compose("/object", router: objectRouter)
}
While this is a fairly contrived example, the pattern of reusing routers in this way is very powerful. Also, composing multiple routers together allows for better project organization, which is very important for bigger applications.
One of the objectives of Zewo is to provide highly-extensible components, and Router
is a great example of this. The way Router
is implemented behind the scenes is through a class called RouterBuilder
, which is passed in to the closure you provide when instantiating the Router
.
By default, RouterBuilder
has support for the following operations:
- get
- options
- post
- put
- patch
- delete
- methods
- fallback
- addRoute
- compose
Assume that you were looking through your codebase and found that the following pattern was being repeated:
route.get("/:id") { request in
guard let id = request.pathParameters["id"] else {
return Response(status: .badRequest)
}
// do something with id
return Response(...)
}
What we want is to overload the get
method and create an API that looks something like this:
route.get { request, id in
// do something with id
return Response(...)
}
Now that we have a target, it's time to get started. The only modification we're really making here is adding another parameter to the handler. To understand how to do this, let's break down the current current decleration of get
:
get(
_ path: String, // the path (ex: "/hello") (removed)
middleware: Middleware..., // middleware (unchanged)
respond: Respond // typealias for Request throws -> Response (modified)
)
Our method is going to be removing the path parameter altogether (it will always be /:id
), and modifying the respond
parameter to be (Request, String) throws -> Response
.
get(
middleware: Middleware...,
respond: (Request, String) throws -> Response
)
The implementation for this is going to be really simple - we're just wrapping around the get
method that is already provided for us.
func get(...) {
// call the default `get` method
get("/:id", middleware: middleware) { request in
// get the id parameter
guard let id = request.pathParameters["id"] else {
return Response(status: .internalServerError)
}
// call the responder with the request and id
let response = try respond(request, id)
// return the result of that responder
return response
}
}
That's it! All we have to do now is put the code in an extension to RouterBuilder
and the code snippet at the beginning of the section will work as expected.
In some (rare) cases, you may want to add functionality to the matching component of the router. For example, what if you want to denote parameters with <param>
instead of :param
, or split on .
instead of /
(like Slack's API)? Or, more likely, what if you want to match routes based on a regular expression instead of something static? To allow this kind of behavior, you can inject your own RouteMatcher
into the Router
upon initialization, like so:
let router = Router(matcher: SomeSpecialMatcher.Self)
By default, the matcher is set to TrieRouteMatcher, which is a high-performance matcher that supports all of the basic route matching functionality (parameters, wildcards, etc.).
To make your own matcher, you must conform to the RouteMatcher
protocol.
public protocol RouteMatcher {
var routes: [Route] { get }
init(routes: [Route])
func match(_ request: Request) -> Route?
}
A basic matcher which only matches exact paths would look something like this:
public struct SimpleRouteMatcher {
let routes: [Route]
init(routes: [Route]) {
self.routes = routes
}
func match(_ request: Request) -> Route? {
for route in routes {
if route.path == request.path {
return route
}
}
return nil
}
}
You can then use the route matcher in your own routers!
Router(matcher: SimpleRouteMatcher.self) { route in
route.get("/hello") { request in
return Response(body: "Hello, world!")
}
}
If you need any help you can join our Slack and go to the #help channel. Or you can create a Github issue in our main repository. When stating your issue be sure to add enough details, specify what module is causing the problem and reproduction steps.
The entire Zewo code base is licensed under MIT. By contributing to Zewo you are contributing to an open and engaged community of brilliant Swift programmers. Join us on Slack to get to know us!
This project is released under the MIT license. See LICENSE for details.