Routes and URL parameters
I prefer the easy-routes library than pure Hunchentoot to define routes, as we did in the tutorial, so skip to its section below if you want. However, it can only be benificial to know the built-in Hunchentoot ways.
Please see the tutorial where we define routes with path parameters and where we also access URL parameters.
Hunchentoot
The dispatch table
The first, most basic way in Hunchentoot to create a route is to add a URL -> function association in its “prefix dispatch” table.
To bind an existing function to a route, we create a “prefix dispatch”
that we push onto the *dispatch-table*
list:
(defun hello ()
(format nil "Hello, it works!"))
(push
(hunchentoot:create-prefix-dispatcher "/hello" #'hello)
hunchentoot:*dispatch-table*)
You just created the /hello
route.
To create a route with a regexp, we use create-regex-dispatcher
, where
the url-as-regexp can be a string, an s-expression or a cl-ppcre scanner.
If you didn’t already, create an acceptor and start the server:
(defvar *server* (make-instance 'hunchentoot:easy-acceptor :port 4242))
(hunchentoot:start *server*)
and access it on http://localhost:4242/hello.html.
We can see logs on the REPL:
127.0.0.1 - [2018-10-27 23:50:09] "get / http/1.1" 200 393 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0"
127.0.0.1 - [2018-10-27 23:50:10] "get /img/made-with-lisp-logo.jpg http/1.1" 200 12583 "http://localhost:4242/" "Mozilla/5.0 (X11; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0"
127.0.0.1 - [2018-10-27 23:50:10] "get /favicon.ico http/1.1" 200 1406 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0"
127.0.0.1 - [2018-10-27 23:50:19] "get /hello.html http/1.1" 200 20 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0"
Easy handlers
The second way to create a route is to use the “easy handlers”.
define-easy-handler allows to create a function and to bind it to an uri at once.
Its form follows
define-easy-handler (function-name :uri <the uri> …) (parameters as a list)
where <the uri>
can be a string or a function.
Example:
(hunchentoot:define-easy-handler (say-yo :uri "/yo") (name)
(setf (hunchentoot:content-type*) "text/plain")
(format nil "Hey~@[ ~A~]!" name))
Visit it at p://localhost:4242/yo and add parameters on the url: http://localhost:4242/yo?name=Alice.
Just a thought… we didn’t explicitly ask Hunchentoot to add this
route to our first acceptor of the port 4242. Let’s try another acceptor (see
previous section), on port 4444: http://localhost:4444/yo?name=Bob It
works too! Our route was configured for the two “acceptors”, the two web apps.
In fact, define-easy-handler
accepts an acceptor-names
parameter. If you are only working one web app, that’s OK. If you want to create the
route for one app only though, you’ll have to use this parameter:
acceptor-names (which is evaluated) can be a list of symbols which means that the handler will only be returned by DISPATCH-EASY-HANDLERS in acceptors which have one of these names (see ACCEPTOR-NAME). acceptor-names can also be the symbol T which means that the handler will be returned by DISPATCH-EASY-HANDLERS in every acceptor.
So, define-easy-handler
has the following signature:
define-easy-handler (function-name &key uri acceptor-names default-request-type) (lambda list parameters)
It also has a default-parameter-type
which we’ll use in a minute to get url parameters.
There are also keys to know for the lambda list. Please see the documentation.
Easy-routes
easy-routes is a route handling extension on top of Hunchentoot. It provides:
- dispatch based on HTTP method (otherwise cumbersome in Hunchentoot)
- arguments extraction from the url path
- and decorators, which help for wrapping the route with more logic (ensure a user is logged in, define the request content type…).
I prefer it to raw Hunchentoot, it is simpler to use.
To use it, don’t create a server with hunchentoot:easy-acceptor
but
with easy-routes:easy-routes-aceptor
:
(setf *server* (make-instance 'easy-routes:easy-routes-acceptor))
Note: there is also
routes-acceptor
. The difference is thateasy-routes-acceptor
iterates over Hunchentoot’s*dispatch-table*
if no route is found byeasy-routes
. That allows us, for example, to serve static content the usual way with Hunchentoot.
Then define a route like this:
(easy-routes:defroute name ("/tasks/:id" :method :get) (debug &get z)
(format nil "we want the task of id: ~a with parameters debug: ~a and z: ~a" id debug z))
You can try it on routes like /tasks/33?debug=t&z=abc
.
Here, :id
captures the path parameter and binds it to the id
variable into the route body. debug
and &get z
define url parameters.
With a :post
request, we could define &post
parameters to extract from the HTTP request
body.
These parameters can take an :init-form
and :parameter-type
options as in define-easy-handler
. The init form gives a default
value if the argument is not supplied, and the parameter type asks
Hunchentoot to convert the argument to this given type. For example,
defining an :integer
will give you an integer and not a string (all
URL parameters are given as a string by default, but more on that on the next section).
easy-routes’ decorators
easy-routes
provides us with an easy way to call any function before
the route body. Following the naming of a popular language, they are
called “decorators”.
In the end route definitions are only functions, right? Decorators are only functions too, but they are run before the route body.
Remember the shape of our routes:
(easy-routes:defroute root ("/") ()
"hello app")
We add a list of decorators after the "/"
part, like this:
(defroute my-protected-route ("/foo" :method :get
:decorators ((@json))) ()
"hello decorated route")
but what is @json
? It’s a function:
(defun @json (next)
(setf (hunchentoot:content-type*) "application/json")
(funcall next))
You can ignore the @
sign, it doesn’t mean anything in Common Lisp
(but as it’s part of the function name it can help you reference all
your route decorators).
Route “decorators” must accept at least one argument: here called next
, it is the
function that will be called next (so, at one point, our route body)
if we want to. Look at (funcall next)
: our decorator correctly
calls it.
If you declare a list of decorators, calling “next” will get you through the chain of decorator functions, and finally to the route body (if no “decorator” exited before).
So what it is function doing? Keep it mind that it is called in the
context of a web request. So we can call hunchentoot:content-type*
(note the *
, this function is applied on the current web
request). We are setting this request’s content-type to
application/json
.
Yes, you can copy-paste the setf
line directly into your function.
Here’s another “decorator”:
(defun @auth (next)
(let ((user (hunchentoot:session-value 'user)))
(if (not user)
(hunchentoot:redirect "/login")
(funcall next))))
Now that’s interesting. It’s doing this:
- it gets a value from the current web session. This can be any Lisp object.
- if a user was registered in the session, we call the
next
method to run other decorators and the route body - otherwise, we redirect to the login page.
We use them in the “User log in” section.
Here’s another decorator from easy-routes’ README:
;; Ensure our PostgreSQL database is connected:
(defun @db (next)
(postmodern:with-connection *db-spec*
(funcall next)))
See the easy-routes
readme for more.
Accessing GET and POST parameters
You probably have nothing to do to get the value of those parameters: if you defined your route with easy-handler
or easy-routes:defroute
, the URL parameters create local variables in the function body. Very much like the arguments of the defun
macro.
However, here’s how to interact more with URL parameters. In particular, we can define the default type of a parameter: they are strings by default, but we can ask to receive an integer.
Hunchentoot and easy-routes URL parameters
First of all, note that we can access query parameters anytime with
(hunchentoot:parameter "my-param")
It acts on the default *request*
object which is passed to all handlers.
There is also get-parameter
and post-parameter
.
Earlier we saw some key parameters to define-easy-handler
. We now
introduce default-parameter-type
.
We defined the following handler:
(hunchentoot:define-easy-handler (say-yo :uri "/yo") (name)
(setf (hunchentoot:content-type*) "text/plain")
(format nil "Hey ~a!" name))
The variable name
is a string by default. Let’s check it out:
(hunchentoot:define-easy-handler (say-yo :uri "/yo") (name)
(setf (hunchentoot:content-type*) "text/plain")
(format nil "Hey ~a you are of type ~a" name (type-of name)))
Going to http://localhost:4242/yo?name=Alice returns
Hey Alice you are of type (SIMPLE-ARRAY CHARACTER (5))
To automatically bind it to another type, we use default-parameter-type
. It can be
one of those simple types:
'string
(default),'integer
,'character
(accepting strings of length 1 only, otherwise it is nil)- or
'boolean
or a compound list:
'(:list <type>)
'(:array <type>)
'(:hash-table <type>)
where <type>
is a simple type.