Why Common Lisp Deserves Another Look: Building Your First Web App
Let's be honest: when was the last time you seriously considered Common Lisp for a web project? If you're like most developers today, your answer probably involves phrases like "ancient history" or "only in university." And honestly, I get it. The syntax looks like parentheses explosions, the documentation can feel sparse, and modern frameworks in Python, JavaScript, and Go have convinced us that web development has to look a certain way.
But here's the thing—Common Lisp is experiencing a quiet renaissance. And the gap between "old language" and "impractical" is smaller than you might think.
The Documentation Problem Nobody Talks About
Before we dive into code, let's address the elephant in the room: documentation for modern Common Lisp web development is genuinely scattered. Unlike Flask's famous "Hello World" tutorial or Django's comprehensive docs, getting started with Lisp web apps often means piecing together information from multiple sources, some of which are decades old.
This isn't because the libraries are bad. On the contrary, tools like Caveman2, Lack, and Clack are well-engineered and actively maintained. The issue is that the community hasn't always prioritized the "getting started" experience—and that creates a real barrier for newcomers.
So let's fix that. Below is a practical guide to building a simple guestbook application. By the end, you'll have a working web app with database connectivity, template rendering, and proper routing. No prior Lisp experience required.
Setting Up Your Environment
First, you'll need a Common Lisp implementation. SBCL (Steel Bank Common Lisp) is the most popular choice and handles performance-critical code beautifully. You'll also want Quicklisp, which serves as your package manager and dependency resolver.
# Install SBCL (macOS)
brew install sbcl
# Install Quicklisp - download from https://www.quicklisp.org/
# Follow the installation instructions
Once installed, fire up your REPL. For the best experience, pair it with SLY (for Emacs) or Alive (for VS Code)—these give you real-time feedback and interactive debugging that makes Lisp development far more approachable.
Creating Your Project Structure
Common Lisp projects have a specific structure, but you don't need to create it manually. The cl-project library generates boilerplate for you:
(ql:quickload :cl-project)
(cl-project:make-project #P"~/guestbook/" :name "guestbook")
This creates a standard project with an src/ directory and guestbook.asd system definition file. The .asd file is your build configuration—think of it as a modern Makefile or package.json, but for compiled Lisp code.
Now, tell ASDF (the build system) where to find your project:
(pushnew #P"~/guestbook/" asdf:*central-registry* :test #'equal)
Choosing Your Stack
For this tutorial, we're using libraries that feel modern and approachable:
- Caveman2: A lightweight web framework with routing and MVC patterns
- Lack: Middleware composition for request/response processing
- Clack: The underlying application server interface
- Djula: Template engine (similar to Jinja2 for Pythonistas)
- cl-dbi: Database abstraction layer with good SQL support
Add these to your depends-on list in the .asd file:
:depends-on (:caveman2
:lack
:clack
:djula
:cl-dbi)
Run (ql:quickload :guestbook) to fetch everything.
Building the Application
The Core Server
Open src/core.lisp (or src/main.lisp in older templates). We'll add functions to start and stop our server:
(defvar *server* nil)
(defparameter *app* nil)
(defun start (&key (port 8080))
"Initialize and start the web server."
(when *server*
(format t "Server already running on port ~D~%" port)
(return-from start))
(setf *app* (lack:builder
(lambda (env)
;; Your routing logic goes here
(list 200
'(:content-type "text/html")
(list "<h1>Guestbook Running</h1>")))))
(setf *server* (clack:clackup *app* :port port))
(format t "Server started on port ~D~%" port))
(defun stop ()
"Gracefully shut down the server."
(when *server*
(clack:stop *server*)
(setf *server* nil)
(format t "Server stopped~%")))
The pattern here is straightforward: *server* holds our running instance, and *app* contains the composed middleware and routing logic. The lack:builder function chains middleware together, much like Express.js middleware or Flask blueprints.
Adding Routes
Now let's make it actually useful. Caveman2 provides a clean routing mechanism:
;; In src/web.lisp
(defpackage :guestbook.web
(:use :cl
:caveman2
:djula)
(:export :*web*))
(in-package :guestbook.web)
;; Define routes
(defroute "/" ()
(render-template* "index.html" ()))
(defroute "/guestbook" (&key |name| |message|)
(if (and |name| |message|)
(progn
(db:save-entry |name| |message|)
(render-template* "success.html" ()))
(render-template* "guestbook.html" ())))
This looks different from what you might be used to, but the semantics are familiar: @app.route('/') in Flask becomes (defroute "/" ...), and query parameters are accessed via &key destructuring.
Database Integration
Let's add persistence with SQLite:
;; In src/db.lisp
(defpackage :guestbook.db
(:use :cl :cl-dbi)
(:export :connect :disconnect :save-entry :get-entries))
(in-package :guestbook.db)
(defvar *connection* nil)
(defun connect ()
"Initialize database connection and create tables."
(setf *connection* (dbi:connect :sqlite3 :database-name "guestbook.db"))
(execute (prepare "CREATE TABLE IF NOT EXISTS entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
message TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP)" *connection*)))
(defun save-entry (name message)
"Save a new guestbook entry."
(execute (prepare "INSERT INTO entries (name, message) VALUES (?, ?)"
*connection*)
name message))
(defun get-entries ()
"Retrieve all guestbook entries."
(let ((query (prepare "SELECT * FROM entries ORDER BY created_at DESC" *connection*)))
(fetch-all (execute query))))
Why Bother?
You're probably wondering: this is interesting, but why would I actually use this? Fair question.
Common Lisp offers three things modern ecosystems struggle to match:
1. Interactive Development at Scale: Because Lisp is interpreted and compiled dynamically, you can modify running code without restarting your server. Change a function, test it immediately, deploy the fix. This workflow is native to Lisp in a way that hot reloading in JavaScript frameworks only approximates.
2. The REPL as a First-Class Tool: Your entire application state is accessible from the command line. Debugging isn't a separate mode—it's woven into how you develop. This isn't nostalgia; it's a genuinely superior development experience once you've felt it.
3. Performance Without Compromise: SBCL compiles to native code and often matches or exceeds C performance for numeric computation. You're not trading productivity for speed.
The Bottom Line
Common Lisp isn't a relic. It's a mature, capable platform that's simply underappreciated by the modern web development community. The documentation gaps are real but closing—and the tools have never been more accessible.
If you've ever wanted to understand why experienced programmers speak reverently about Lisp, building a real application is the best way to find out. The parentheses fade into the background surprisingly quickly, and what remains is a language that respects your intelligence and rewards deep familiarity.
Give it an afternoon. You might be surprised what you build.
Ready to start? Set up SBCL, load Quicklisp, and join the small but enthusiastic community of developers keeping Lisp relevant. Your guestbook is waiting.
Read in other languages: