Common Lisp web development and the road to a middleware
I’ve been looking recently for a Common Lisp system, which would allow me to expose a service I am developing over a REST API and honestly the options available right now in the Common Lisp ecosystem may be a bit overwhelming at first.
If you are like me and going through the same exercise of finding a
system to develop a simple REST API you will be hearing a lot about
hunchentoot
, lack
, clack
, caveman
, caveman2
, ningle
,
snooze
, radiance
, cl-rest-server
, woo
, wookie
, etc.
The list goes on and on … Prepare yourself for some reading what exactly each system is and what it is meant to be used for. Hint: some of these are web servers, others are web frameworks, and others are micro web frameworks.
The Common Lisp Cookbook does provide a good introduction to the various systems and what they do, so make sure to check it out.
The State of Common Lisp, 2022 also provides a good overview of the different options at your disposable, but after reading it you will probably be even more confused as to which one to use.
The Why Hunchentoot instead of Clack? article also makes some good points to think about when considering one or the other, so check that one as well.
In my case I was looking for a simple web framework, which would allow me to develop a simple REST API. Something along the lines of ring in Clojure, which I’ve used before, when I was doing some Clojure.
With that said I’ve looked into
caveman first, as this seemed
to be what people have been recommending. caveman
is a
“batteries-included” web framework for Common Lisp. It appears to be
the Django for Common Lisp, and
comes with nice goodies such as template systems, database
integration, configuration system, project skeletons, routing macros,
etc. I’ve tried out caveman
and while I do liked it, it is a bit too
much for my simple use cases.
Another thing to keep in mind about caveman
is that it is based on
clack
, which is based on lack
. What that means is that now you
have to learn all the abstractions the framework is built on top of.
Again, comparing that to ring
it is a bit of an overkill to me, so I’ve decided to keep looking
elsewhere for now.
One thing worth mentioning here is that if you decide to use clack
as a web framework it abstracts away the web server specifics by
providing a unified API. What that means is that you could be running
hunchentoot
or other servers from your clack
app, which is a nice
thing.
After some time (and lots of reading and testing things out), I’ve reduced the list of systems I want to evaluate more to just two – snooze and ningle.
Both of them provide interesting features, and approach the same
problems in different ways. For example in snooze
a route is modeled
around CLOS and uses generic functions and the accompanying methods,
which is really nice, because you could decorate your HTTP routes with
:before
, :after
and :around
methods. I really like that one! I’m
planning to spend more time soon with snooze
, but on the surface -
it seems to be what I look for.
ningle
is nice as well, and one of the features that are interesting
to me is the ability to define additional requirements per
route. Getting
started with ningle
is easy when following the README documentation,
but you will need to read more about
clack and
lack as well in order to fully
understand the design choices in ningle
. An honestly clack
doesn’t
provide much of a documentation, so this makes it a bit of a challenge
initially. Compare that with the ring
documentation, which is
great in my opinion, as it provides everything you need to get
started.
Okay, this opening paragraph ended up much longer than what I’ve planned initially, but I wanted to give you a bit more context as to what I was going for.
In a future post I may write more about my experience with web development in Common Lisp, but before that let’s get to the point of this post – how to create custom middlewares in ningle, as this has been something I really wanted to get working, and the system doesn’t support it.
First, what is a middleware? A middleware is a function which wraps an
existing application and returns a new application. A typical use case
for a middleware is to provide logging capabilities for your web
service, where each request/response is logged as it passes through
between the client and the server. Other use cases for middlewares is
the ability to push (or inject) additional context to your HTTP route
handlers, e.g. a database connection which will be re-used by all HTTP
handlers (routes in ningle
terms).
The documentation of ningle, clearly states that “you can use other Lack middlewares with ningle”.
While that is true, and you can use middlewares in ningle
, and these
middlewares can interact with the surrounding HTTP request
environment/context before they hit the actual HTTP handler/route, it
appears that the HTTP handlers/routes in ningle
are somewhat limited
when comparing them with a handler/route written in lack
.
Here’s an example of a HTTP route/handler from the lack documentation:
(lambda (env)
(declare (ignore env))
'(200 (:content-type "text/plain") ("Hello, World")))
It is just a regular lambda function, which takes a single argument –
the environment of your
app. Now, you can
use this environment, and in fact that’s what middlewares do in order
to push additional context to your app. This is what a typical
middleware looks like in lack
.
(defun middleware (app)
(lambda (env)
;; preprocessing
(let ((res (funcall app env)))
;; postprocessing
res)))
You can then use this middleware to wrap an existing app and provide a different behaviour when your route is called.
You could create a middleware that looks like this.
(defun middleware-message (app)
(lambda (env)
(setf (getf env :message) "A message from the environment")
(funcall app env)))
If you enable this middleware using lack:builder
now you can access
the message that was pushed into the environment by our middleware.
(lack:builder
#'middleware-message
(lambda (env)
`(200 () (getf env :message))))
While this is a pretty contrived example, I hope you get the idea. You could use middlewares for things such as pushing a database connection, so that your HTTP routes can use it, when needed.
So, back to ningle
. In ningle
the HTTP handler/route looks like
this.
(defvar *app* (make-instance 'ningle:app))
(setf (ningle:route *app* "/hello/:name")
#'(lambda (params)
(format nil "Hello, ~A" (cdr (assoc :name params)))))
Again we see the well-known lambda handler, but this time the
lambda-list is different – your ningle
route will receive the HTTP
request params, and not the surrounding environment. And that is a bit
of an issue here, because if you want to create a custom middleware,
which touches the environment there is no way your route will ever be
able to get to it.
The README provides a section about ningle context, which seems to be what you should use, but after trying that out that doesn’t seem to be the case. Here’s the example from the README.
(setf (context :database)
(dbi:connect :mysql
:database-name "test-db"
:username "nobody"
:password "nobody"))
(context :database)
;;=> #<DBD.MYSQL:<DBD-MYSQL-CONNECTION> #x3020013D1C6D>
You may end up with the impression that this thing will set things up
for you and you can use the context
from within the routes and you
will be right. The problem is that (setf (ningle:context ...))
works
only within the routes. And that is too late in the game already.
The funny thing is that the (ningle:context)
is derived from the
same environment
that we’ve seen before in the lack
routes, but it
is stripped down to just a few things – the request
, response
and
the session
, which is set up by the lack.middleware.session
middleware.
So, to sum it up – if you need to create custom middlewares in
ningle
, which interact with the environment
you are stuck, because
your HTTP routes won’t be able to use it. This appears to be a known
thing and was reported ~ 7 years
ago. Before I
completely scrap ningle
of the list of frameworks I’d like to use
I’ve decided to give it a stab and see if I can make it work.
Checking the code, the method of interest is this
one,
which defines a method for lack:call
of our ningle app.
(defmethod call ((this app) env)
"Overriding method. This method will be called for each request."
(declare (ignore env))
(multiple-value-bind (res foundp)
(dispatch (mapper this) (request-path-info *request*)
:method (request-method *request*))
(if foundp
res
(not-found this))))
As you can see the environment
is being ignored here, and if you
look at the code of DISPATCH
function you will find the parts which
invoke our routes and pass down the parsed HTTP request params.
So, to fix this I’ve created a new sub-class of ningle:app
which
overrides the lack:call
method, but also dynamically binds the
environment
, so that at least it is present when HTTP routes need
it. So, time to make things work. Fire up your REPL and load the
systems we will use.
(ql:quickload :ningle)
(ql:quickload :lack)
(ql:quickload :clack)
And now we will define our sub-class of ningle:app
and provide our
custom implementation of lack:call
. The important bits here are that
we are dynamically binding *REQUEST-ENV*
.
(defparameter *request-env* nil
"*REQUEST-ENV* will be dynamically bound to the environment context
of HTTP requests")
(defclass app (ningle:app)
()
(:documentation "Custom application based on NINGLE:APP"))
(defmethod lack.component:call ((app app) env)
;; Dynamically bind *REQUEST-ENV* for each request, so that ningle
;; routes can access the environment.
(let ((*request-env* env))
(call-next-method)))
Here’s an example middleware we are going to use, which pushes a message to the environment, which will later be used by our example route.
(defun my-middleware (app)
"A custom middleware which wraps a NINGLE:APP and pushes additional
metadata into the environment for HTTP routes.
(lambda (env)
(setf (getf env :my-middleware/message) "my middleware message")
(funcall app env)))
Okay, that’s pretty much it. Time to create an app instance and test things out.
(defparameter *app* (make-instance 'app))
(setf (ningle:route *app* "/")
(lambda (params)
(declare (ignore params))
(getf *request-env* :my-middleware/message)))
(defparameter *wrapped-app* (lack:builder #'my-middleware *app*))
(defparameter *server* (clack:clackup *wrapped-app*))
This should get your app started and listening on the default 5000
port. Finally we can query our simple API and see if things work.
$ curl -X GET -vvv http://localhost:5000/
* Trying 127.0.0.1:5000...
* Connected to localhost (127.0.0.1) port 5000 (#0)
> GET / HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.86.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Thu, 01 Dec 2022 17:48:18 GMT
< Server: Hunchentoot 1.3.0
< Transfer-Encoding: chunked
< Content-Type: text/html; charset=utf-8
<
* Connection #0 to host localhost left intact
my middleware message
Great, things work as expected now and we are able to interact with
the environment
, so our middlewares can do their job. Ideally, this
should be fixed in ningle
itself, as it makes more sense to be that
way in my opinion.
You can find the full code here.
Next, I will be reviewing snooze
, before I make up my mind between
ningle
and snooze
. Till next time!