A simple Common Lisp web app (2025)

tosh1 pts0 comments

A simple Common Lisp web app A simple Common Lisp web app<br>April 30, 2025

One of the drawbacks I’ve found when dealing with Common Lisp is the lack of documentation available. Too often, I find published libraries without an explanation of how they are meant to be used or only partially documented, and I need to dig into the source code to understand what they do and to see all the functions available. Even though reading source code is a proven technique to improve one’s grasp of a programming language, most other systems come with extensively documented libraries, something appreciated by beginners and a factor that often contributes to a language’s popularity.

In my opinion, this lack of good documentation is one of the reasons Common Lisp is often seen as challenging for beginners, which makes it harder for the language to become popular.

xkcd.com/224

Some time ago, when I looked for guidance on writing a generic web app, I was surprised by the absence of a quickstart page to help me set up a simple server—something the Python community has provided for Flask for many years.

“The most underrated skill to learn as an engineer is how to document. Fuck, someone please teach me how to write good documentation. Seriously, if there’s any recommendations, I’d seriously pay for a course (like probably a lot of money, maybe 1k for a course if it guaranteed that I could write good docs.)”<br>A drunk dev on reddit

So I put together a short tutorial on how to build a simple web app in Common Lisp, inspired by the Clojure tutorial written for Luminus. The goal is to write a guestbook demo, involving template rendering, connecting to a database to run queries, and exposing routes to the webpage.

To follow the tutorial, you need a Common Lisp implementation, like SBCL, with Quicklisp, the dependency manager. I also recommend having a REPL integrated with your IDE, like SLY for Emacs or Alive for VSCode.

I’ll use more modern CL libraries that have an interface similar to other languages, so it might be a bit easier to follow along.

The server

First things first, I created a new Common Lisp project. To do that with a simple boilerplate, I loaded cl-project in the environment and called the function make-project with a path and a name for the project.

> (ql:quickload :cl-project)

To load "cl-project":

Load 1 ASDF system:

cl-project

; Loading "cl-project"

..

(:CL-PROJECT)

> (cl-project:make-project #P"~/guestbook/" :name "guestbook")

writing ~/guestbook/guestbook.asd

writing ~/guestbook/README.org

writing ~/guestbook/README.markdown

writing ~/guestbook/.gitignore

writing ~/guestbook/src/main.lisp

writing ~/guestbook/tests/main.lisp

(ql:quickload :cl-project)To load "cl-project": Load 1 ASDF system: cl-project; Loading "cl-project"..(:CL-PROJECT)> (cl-project:make-project #P"~/guestbook/" :name "guestbook")writing ~/guestbook/guestbook.asdwriting ~/guestbook/README.orgwriting ~/guestbook/README.markdownwriting ~/guestbook/.gitignorewriting ~/guestbook/src/main.lispwriting ~/guestbook/tests/main.lispT">

Then I ran this command to let Quicklisp know where my new project is located.

> (pushnew #P"~/guestbook/" asdf:*central-registry* :test #'equal)

(pushnew #P"~/guestbook/" asdf:*central-registry* :test #'equal)">

I declared the required libraries in the :depends-on property so that Quicklisp would download them from the repo and load them into the environment.

guestbook.asd(defsystem "guestbook"

:version "0.0.1"

:license "MIT"

:depends-on (:alexandria ;; utils

:uiop

:cl-ppcre ;; regex library

:cl-syntax-annot ;; for @export annotation

:clack ;; Web libraries

:lack

:caveman2 ;; Web framework

:djula ;; Template engine

:cl-dbi) ;; Database

:components ((:module "src"

:components

((:file "config") ;; files into src/

(:file "db")

(:file "web")

(:file "core"))))

Now, inside src/core.lisp (I renamed the file from main to core) I added two new functions, to start and stop the server, which will be helpful to use from the REPL.

src/core.lisp(defvar *server* nil)

(defparameter *app*

(lack:builder

(:static

:path (lambda (path)

(if (ppcre:scan

"^(?:/images/|/css/|/js/|/robot\\.txt$|/favicon\\.ico$)"

path)

path

nil))

:root *static-directory*)

;; Additional middlewares

guestbook.web:*web*))

@export

(defun start (&rest args

&key

(server :hunchentoot)

(port 3210)

(debug nil)

&allow-other-keys)

"Starts the server."

(when *server*

(restart-case (error "Server is already running.")

(restart-server ()

:report "Restart the server."

(stop))))

(setf *server* (apply #'clack:clackup *app*

:server server

:port port

:debug debug

args))

(format t "Server started"))

@export

(defun stop ()

"Stops the server."

(when *server*

(clack:stop *server*)

(format t "Server stopped")

(setf *server* nil)))

*server* contains the server instance and is defined as a variable since we will need to redefine it. *app* is the web application wrapped with a layer by Lack....

guestbook server project lisp writing common

Related Articles