Chapter 3: Routers

We’ve already used Kitura’s Router class quite a bit in this book, and you might be thinking that you’ve already got a pretty good idea of how it works. Well, if that’s what you think… you’re probably right. Nonetheless, there are still a few new tricks we can learn.

HTTP Methods

As previously mentioned, routers have methods (in the Swift sense) for defining routes and handlers which correspond to various HTTP methods (also sometimes called HTTP verbs) available. In most cases, get() and post() are the only ones you’ll use, which obviously correspond to the GET and POST HTTP methods. But if you want to be really RESTy, there’s also put() and options() and lock() and a bunch more available to you. See the RouterHTTPVerbs_generated.swift file in the Kitura project for all of them; the code in this file is so monotonous that, as the filename implies, it was actually generated by a script rather than written by hand. (The script itself is at Scripts/generate_router_verbs.sh in the Kitura project directory.) Use these methods to define routes and corresponding handlers that will only fire if a request using the corresponding HTTP method is made from the client. Let’s test that.

router.get("/get-only") { request, response, next in
    response.send("GET Success!\n")
    next()
}

Let’s try accessing that route with both GET and POST requests.

$ curl localhost:8080/get-only
GET Success!
$ curl -d "" localhost:8080/get-only
Cannot POST /get-only.

And let’s try that again with one of the more unusual methods.

router.lock("/lock-only") { request, response, next in
    response.send("LOCK success!\n")
    next()
}

Curl’s --request option will let us define any arbitrary HTTP method to send in the request, so let’s use that when we test.

$ curl --request LOCK localhost:8080/lock-only
LOCK success!
$ curl -d "" localhost:8080/lock-only
Cannot POST /lock-only.
$ curl localhost:8080/lock-only
Cannot GET /lock-only.

Okay, so nothing too surprising there.

Aside from these methods, you may recall in an earlier example that we used all(). Setting a handler with this method will cause the handler to fire no matter what HTTP method was used to access the path. This bit of code may look a little familiar:

router.all("/request-info") { request, response, next in
    response.send("The request method was \(request.method.rawValue).\n")
}

Let’s test it out.

$ curl localhost:8080/request-info
The request method was GET.
$ curl -d "" localhost:8080/request-info
The request method was POST.
curl --request UNSUBSCRIBE localhost:8080/request-info
The request method was UNSUBSCRIBE.

The request type still has to be one included in Kitura’s RouterMethod enum, however, so we can’t get too crazy.

$ curl --request BEANSANDRICE --include localhost:8080/request-info
HTTP/1.1 400 Bad Request
Date: Sun, 03 Sep 2017 03:20:36 GMT
Connection: Close

If you enabled logging, you’ll also see “Failed to parse a request. Parsed fewer bytes than were passed to the HTTP parser” logged. Note that, despite the wording of the error, if you see it in the future, it may be because you’re using an incorrect HTTP method to make a request to your server. And remember, capitalization counts!

At any rate, I generally would not recommend using all() in most cases and instead use get(), post() and friends. If you’re using all(), then either you want the same thing to happen no matter what HTTP method was used to access a path, which is incorrect behavior speaking on a technical level and just generally silly, or you’ll have to implement a convoluted logic fork in your code along the lines of…

router.all("/some-path") { request, response, next in
    switch request.method {
    case .get:
        // Do something
        break
    case .post:
        // Do something else
        break
    default:
        // Respond with a 404 error
        break
    }
    next()
}

But this sort of thing is unnecessary. Remember way back in the first chapter when I mentioned that paths can have more than one handler assigned to them? The same holds true no matter what HTTP method you’re telling that path to use. So the above can be replaced with the following much nicer code:

router.get("/some-path") { request, response, next in
    // Do something
    next()
}

router.post("/some-path") { request, response, next in
    // Do something else
    next()
}

// Requests to "/some-path" with methods other than GET and POST will still
// automatically result in a "404 Not Found" response.

Path Parameters

So far in this book, we have used static paths with our routes. However, dynamic paths are pretty easy to implement with Kitura using path parameters.

For example, consider a blog that uses a path like “blog/1” to show the first blog post, “blog/2” to show the second one, and so on. Now obviously it would be unwieldy to implement this in Kitura using static paths.

router.get("/post/1") { request, response, next in
    // Load and show post 1
    next()
}

router.get("/post/2") { request, response, next in
    // Load and show post 2
    next()
}

// …

Instead, we can use a path with a parameter. To do so, add a path segment with a name prefixed with a colon character. You can then find the value of the parameter used to access the path by looking it up by that name in the parameters property of the RouterRequest object passed to your handler; parameters is a simple [String: String] dictionary.

router.get("/post/:postId") { request, response, next in
    let postId = request.parameters["postId"]!
    response.send("Now showing post #\(postId)\n")
    // Load and show the post
}

Let’s test.

$ curl localhost:8080/post/4
Now showing post #4

Note that you can easily use more than one parameter in your paths, and they don’t have to be at the end of the path.

router.get("/:authorName/post/:postId") { request, response, next in
    let authorName = request.parameters["authorName"]!
    let postId = request.parameters["postId"]!
    response.send("Now showing post #\(postId) by \(authorName)\n")
    // Load and show the post
}

We test, and it works as expected.

$ curl localhost:8080/Nocturnal/post/4
Now showing post #4 by Nocturnal

Okay, that’s pretty cool. But let’s go back and look at that simpler path parameter example one more time.

router.get("/post/:postId") { request, response, next in
    let postId = request.parameters["postId"]!
    response.send("Now showing post #\(postId)\n")
    // Load and show the post
}

There’s a potential problem here in that the postId parameter can be anything. For example…

$ curl localhost:8080/post/hello
Now showing post #hello

Okay, no sweat, right? If we want to make sure the post ID is a positive number, we can just do something like…

router.get("/post/:postId") { request, response, next in
    guard let postId = request.parameters["postId"], let numericPostId = UInt(postId) else {
        response.status(.notFound)
        response.send("Not a proper post ID!\n")
        return
    }
    response.send("Now showing post #\(numericPostId)\n")
    // Load and show the post
}

And, yes, this works well enough.

$ curl localhost:8080/post/hello
Not a proper post ID!

But there’s another way. We can use regular expressions to define that we want a path parameter to fit a certain format. So let’s do it that way instead. To implement a path parameter with a regular expression, name it with a colon as normal, but then follow the name with the regular expression pattern in parentheses. The pattern to match one or more digits is \d+, but we need to escape that backslash with another backslash. So let’s implement it this way.

router.get("/post/:postId(\\d+)") { request, response, next in
    let postId = request.parameters["postId"]!
    response.send("Now showing post #\(postId)\n")
    // Load and show the post
}

Now let’s test. You’ll see that trying a non-numeric path parameter now causes Kitura to return its standard 404 Not Found error; we didn’t have to write any extra code in our handler to make it happen.

$ curl localhost:8080/post/hello
Cannot GET /post/hello.
$ curl localhost:8080/post/85
Now showing post #85

Note that you don’t want to use the ^ and $ regular expression tokens in your pattern to signify the beginning and end of the path parameter value; they are effectively implicitly added by Kitura.

$ curl localhost:8080/post/3-bananas
Cannot GET /post/3-bananas.

results matching ""

    No results matching ""