Routes: URL-based dispatching

Routes are a powerful concept in web application design. Think of routes families of URL resources, where the families are distinguished from one another by straightforward URL patterns. Here’s how you can build a route-based web applications in Racket.

Sample code used in this chapter: chapter-02.rkt, chapter-02-location.rkt. respond.rkt is introduced in chapter 1 and used here.

dispatch-rules and friends

If you like, you can manually set up dispatching rules for your requests. Go ahead and write a giant cond and check the value of request-uri for incoming requests, directing the request to other servlets.

But there’s already a nicer mechanism built-in and ready for use: dispatch-rules.

dispatch-rules and friends take as input a description (one might even call it a mini-DSL) of what your URL rules should look like. The rules take the form of routes—parameterized families of URLs. You specify what the URL looks like (possibly including placeholders), what request method is used, and, finally, a servlet that is responsible for handling precisely these kinds of requests.

Let’s make that concrete. Here’s a very simple web application that has, essentially, only one resource that says hello in a few different ways. It will be available under /hello. We can use GET to access this resource; any other HTTP method should lead to an error. Accessing any other resource leads to an error, too.

Let’s use the respond function (defined in the previous chapter) to define servlets that do nothing but generate empty error responses: for attempting to access a resource using a disallowed HTTP method:

;; request? -> response?
(define (method-not-allowed req) (respond #:code 405))

And for accessing an unknown resource:

;; request? -> response?
(define (not-found req) (respond #:code 404))

(Notice that we don’t even use the request argument. We will see how the dispatcher ensures that this makes sense.)

Our list of greetings is as follows:

(define greetings
  (list "Hi!"
        "Hello!"
        "Hallo!"
        "¡Hola!"
        "Ola!"
        "こんにちは"
        "مرحبا"))

The servlet corresponding to the hello resource is then, simply:

;; request? -> response?
(define (hello req) (respond #:code 200 #:mime "text/plain;charset=UTF-8" #:body (random-greeting)))

where random-greeting is defined as:

;; -> string?
(define (random-greeting) (list-ref greetings (random num-greetings)))

The final piece of the puzzle, the dispatcher, is defined as follows:

(define-values (dispatcher _)
  (dispatch-rules
   [("hello")
    #:method "get"
    hello]
   [("hello")
    #:method (regexp ".*")
    method-not-allowed]
   [else
    not-found]))

With the dispatcher defined this way, we’re saying that we accept, essentially, any request whatsoever (the else clause at the end ensures that). If the request URI is /hello, then we either (a) use the hello servlet, if the request method is GET, or (b) bail out and return a 405 (Method Not Allowed) response, if the any other request method is used. Otherwise, we return a 404 (Not Found) response.

Extracting a URL from placeholders

The dispatch-rules returns two values. We’ve spent some time talking about the first—the dispatcher that handles the mini-DSL for specifying routes—but none so far about the second. We’ve ignored it.

What’s great about dispatch-rules is that its second return value is a kind of inverse of the first.

Here’s what I mean. The first value—the dispatcher—is a function that takes as input an HTTP request, chops up the URL (and uses the HTTP method), and decides where the request should go. Roughly speaking, then, the dispatcher take a URL and returns a route.

The second value is a URL generator that takes a route, together with its arguments, and generates a URL.

Let’s see how that works. (This demonstration code can be found in location.rkt, not in the usual main.rkt.)

Let’s tag languages by a two-letter ISO 639-1 language code. We will still have the usual hello resource, which accepts GET requests. It generates a random plain text greeting. In addition, it returns a Location header exposing a URL where precisely this greeting—as opposed to a random one—can be found.

The raw greetings data now takes the form of a hash table:

(define greetings/hash
  (hash "en" "Hello!"
        "de" "Hallo!"
        "es" "¡Hola!"
        "pt" "Ola!"
        "jp" "こんにちは"
        "ar" "مرحبا"))

When hello is requested (via GET), we pick a random greeting by first picking a random language, and then using the hash table to find the greeting:

;; -> string?
(define (random-language) (list-ref languages (random num-languages)))

where languages and num-languages are defined as

(define languages
  (hash-keys greetings/hash))

and

(define num-languages
  (length languages))

The dispatcher that we want to use looks like this:

(define-values (dispatcher url-generator)
  (dispatch-rules
   [("hello")
    #:method "get"
    hello]
   [("hello" (string-arg))
    #:method "get"
    hello+lang]
   [("hello")
    #:method (regexp ".*")
    method-not-allowed]
   [else
    not-found]))

The magic hasn’t yet showed up, but we’re approaching it. Notice that we provide the hello resource, as before. But we allow a second, parameterized variant of it. The idea is that the argument to hello will take a language code and return a greeting in that language.

We use url-generator in the definition of the no-argument version of hello:

;; request? -> response?
(define (hello req) (define lang (random-language)) (define greeting (hash-ref greetings/hash lang)) (respond #:code 200 #:mime "text/plain;charset=UTF-8" #:headers (list (cons 'Location (url-generator hello+lang lang))) #:body greeting))

To generate a fixed (as opposed to random) greeting, the hello+lang servlet is used:

;; request? -> response?
(define (hello+lang req lang) (define greeting (hash-ref greetings/hash lang #f)) (cond ((string? greeting) (respond #:code 200 #:mime "text/plain;charset=UTF-8" #:body greeting)) (else (not-found))))

With this setup, submitting GET requests for /hello will yield a random greeting, but with a suitable Location header:

$ http GET http://localhost:6995/hello

with the response

HTTP/1.1 200 OK
Content-Length: 15
Content-Type: text/plain;charset=UTF-8
Date: Wed, 29 Nov 2017 05:26:44 GMT
Last-Modified: Wed, 29 Nov 2017 05:26:44 GMT
Location: /hello/jp
Server: Racket

こんにちは

And again:

$ http GET http://localhost:6995/hello

HTTP/1.1 200 OK
Content-Length: 6
Content-Type: text/plain;charset=UTF-8
Date: Wed, 29 Nov 2017 05:27:26 GMT
Last-Modified: Wed, 29 Nov 2017 05:27:26 GMT
Location: /hello/de
Server: Racket

Hallo!

And if we submit a GET request for /hello/de, we will get a non-random result identical (ignoring the Date and Last-Modified headers) to the previous output. Similarly for /hello/jp, /hello/en, and so on.

Want more Racket web devel goodness?

You’ve just read chapter 2 of Server: Racket—Practical Web Programming with the Racket HTTP Server. If you’d like to get the whole book, it may be purchased here.