Finding Joy: Handlers
May 9, 2020In part 1, we created a very simple web app:
(import halo)
(defn hello [request]
{:status 200 :body "Hello, world!" :headers {"Content-Type" "text/plain"}})
(halo/server hello 8080)
Let’s dig into this.
First we import the halo
module. The halo module contains the server
function which starts the server up and listens for requests. We can call it by prefixing it with the module name: halo/server
. The server function takes 2 arguments: a handler function, which we have defined as hello
, and a port to run on.
A handler function is a function that takes a request table and returns a response table or struct:
Our hello
handler returns a struct containing the following http response elements:
- status
- body
- headers
When this simple app gets run, Halo will start up on port 8080 and respond to every request by calling our hello
handler function.
If we run it:
janet hello.janet
Server listening on [localhost:8080] ...
and use curl to make a request:
$ curl -i http://localhost:8080
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 13
Hello, world!
We can see our 200 in the status line, our Content-Type: text/plain header, and the “Hello, world!" body we set.
The basic foundation for the back end of any Halo/Joy web app is returning a table or struct with those 3 elements. Pretty straightforward, right?
There’s a problem though: right now our simple web app will return “Hello, world!” for any URL we request:
$ curl http://localhost:8080
Hello, world!
$ curl http://localhost:8080/foo
Hello, world!
$ curl http://localhost:8080/bar/baz
Hello, world!
If we want to respond differently to different requests, we somehow have to know what URL path is being used. Let’s examine the request parameter that our handler receives. Janet has a string/format function that we can use to convert the request parameter into something we can return in a :body
. The %q
format specifier can be used to turn any data structure into a string. Let’s change our app to return the request as a string:
(import halo)
(defn hello [request]
{:status 200 :body (string/format "%q" request)})
(halo/server hello 8080)
The :headers
element has been removed to keep things simple. It won’t break anything. Let’s run the server again:
janet hello.janet
Server listening on [localhost:8080] ...
then in another tab try calling different URLs:
$ curl http://localhost:8080
@{:method "GET" :uri "/" :headers @{"User-Agent" "curl/7.54.0" "Accept" "*/*" "Host" "localhost:8080"}}
$ curl http://localhost:8080/foo
@{:method "GET" :uri "/foo" :headers @{"User-Agent" "curl/7.54.0" "Accept" "*/*" "Host" "localhost:8080"}}
$ curl http://localhost:8080/bar/baz
@{:method "GET" :uri "/bar/baz" :headers @{"User-Agent" "curl/7.54.0" "Accept" "*/*" "Host" "localhost:8080"}}
Here we can see that the request
Halo is passing to our hello handler is a table (note the @
preceding the braces) containing 3 elements: :method
, :uri
, and :headers
, which is also a table.
If we want to respond to different request paths, we can use the :uri
value to do so. Let’s try:
(import halo)
(defn hello [request]
(case (request :uri)
"/" {:status 200 :body "Root"}
"/foo" {:status 200 :body "Foo"}
"/bar/baz" {:status 200 :body "Bar Baz"}
{:status 404})))
(halo/server hello 8080)
A couple of things here:
- The case macro takes a value and matches it against a set of pairs. Each pair contains a value to match against (the first one is
"/"
) and the value to return if a match is made ({:status 200 :body "Root"
} for"/"
). A default value can be added at the end when no match is made. In our case, we are defaulting with the http status 404. - The case is matching on the result of calling
(request :uri)
. Janet has a nifty feature that allows tables to be used as functions. It’s similar to calling(get request :uri)
.
If we call the server with no path, we should get “Root” in the body:
Let’s restart the server and try visiting the different urls:
$ curl http://localhost:8080
Root
$ curl http://localhost:8080/foo
Foo
$ curl http://localhost:8080/bar/baz
Bar Baz
$ curl -i http://localhost:8080/quux
HTTP/1.1 404 Not Found
Note on the last curl call, the i
flag was used to print out headers to verify that the 404 status code was returned.
Nice–now we know how to respond to any request path. The way we’re handling it is kinda clunky though. In a future post we’ll define a better way to do this.
#janet #joy #web #lisp