Decanter

Installation

We recommend using the latest version of SBCL.

Note that you will need libev installed to run Woo, the application server that Decanter uses. libev is available from most Linux distribution package managers, and on Homebrew.

Quicklisp

The easiest way to install Decanter is via Quicklisp, which you will need to install first.

However, we don't ship software in the core Quicklisp distribution, so Decanter is only available on Ultralisp.

Ultralisp

Enable the Ultralisp distribution:

(ql-dist:install-dist "http://dist.ultralisp.org/")

Install Decanter

Installing Decanter with Quicklisp will pull all dependencies and make them available to your Lisp implementation.

(ql:quickload :decanter)

Quickstart

It's easy to get up and running with Decanter; a swift tour of its functionality follows.

A Minimal Application

We recommend developing a Decanter application with a Lisp REPL and packaging it into an ASDF system. It is also possible to run a Decanter application as a standalone shell script or standalone binary.

A minimal Decanter application looks like this:

(require :decanter)
(use-package :decanter)

(defurls *urls*
    '("/" hello))

(defapp *app* *urls*)

(defhandler hello
  (:get () "Hello, world!"))

What does this code do?

  1. We require and use the decanter package. This package contains the Decanter micro framework and its components.
  2. We use the defurls macro to create a mapping between a URL pattern and a handler function. In this case the pattern is the literal "/" and the handler function is called hello.
  3. We use the defapp macro to create an instance of Decanter's application class, which stores the URL mapping and other data.
  4. We use the defhandler macro to create the handler function hello. Here we specify how the function handles an HTTP GET request: we simply respond with the string "Hello, world", which will be displayed by the user's browser.

Start the application like this:

(run *app*)

This launches the Woo web server, which you may want to place behind a reverse proxy when deploying in production.

Next, go to http://localhost:5000, and you should see the Hello, world! greeting.

Note that by default the server is only accessible from your own computer, not from any others in the network. This is the default behavior. If you want to make the application available to other computers on your network, instead start it like this:

(run *app* :address "0.0.0.0")

Doing so tells your operating system to listen on all public IPs.

You can stop the application like this:

(stop *app*)

Debug Mode

Decanter does come with a debug mode, but right now it's fairly rudimentary. It is enabled by default when using the defapp macro to create a Decanter application. The debug mode prints HTTP requests and HTTP responses in a human-readable format.

An HTTP request:

------------
HTTP REQUEST
------------
request:
method: :GET
target: "/"
headers: (:CONTENT-TYPE NIL
          :CONTENT-LENGTH NIL
          :PRIORITY "u=1"
          :SEC-FETCH-USER "?1"
          :SEC-FETCH-SITE "none"
          :SEC-FETCH-MODE "navigate"
          :SEC-FETCH-DEST "document"
          :UPGRADE-INSECURE-REQUESTS "1"
          :COOKIE "lack.session=e8e77fe4d6c9065d3b249fe00f0a243757dabfa5"
          :CONNECTION "keep-alive"
          :DNT "1"
          :ACCEPT-ENCODING "gzip, deflate, br, zstd"
          :ACCEPT-LANGUAGE "en-US,en;q=0.5"
          :ACCEPT "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"
          :USER-AGENT "Mozilla/5.0 (Windows NT 10.0; rv:126.0) Gecko/20100101 Firefox/126.0"
          :HOST "localhost:5000")
body: ""
cookies: NIL

An HTTP response:

-------------
HTTP RESPONSE
-------------

response: (200 (:CONTENT-TYPE "text/html") ("Hello, world!"))

You may wish to disable debug mode when exposing a Decanter application to your network or deploying in production.

You can do so when creating an application:

(defapp *app* *urls* :debug nil)

Or with an application that already exists:

(setf (application-debug *app*) nil)

HTML Escaping

When returning HTML, which is the default response type in Decanter, any user-provided value rendered in the output must be escaped to prevent injection attacks. HTML rendered with Spinneret, introduced later, has this done automatically.

The escape-string function can be used manually. It is omitted in most examples for brevity, but always be aware of how you're using untrusted data.

(defurls *urls*
    '("/" hello))

(defapp *app* *urls*)

(defun hello-html ()
  (with-html-string
    (:form :action "/" :method "post"
           (:label :for "name" "Name:")
           (:input :type "text" :name "name"))))

(defhandler hello
  (:get () (hello-html))
  (:post (name) (escape-string name)))

If a user managed to submit the name <script>alert("bad")</script>, escaping causes it to be rendered as text, rather than running the script in the user's browser.

name in the hello handler function is a variable captured from the webform defined in hello-html. These variable rules are explained below.

Routing

Modern web applications use clean, memorable URLs. Decanter makes it easy to work with them.

Use the defurls macro to create the URL mapping for a new Decanter application:

(defurls *urls*
    '("/(.*)" :regex placeholder
      "/" index
      "/hello" hello))

(defapp *app* *urls*)

Or, if the application already exists, use the add-mapping method:

(add-mapping *app* "/hello-world" hello)

(add-mapping *app* "/hello/(.*)" hello-name :regex t)

Each URL mapping associates a URL pattern specified by a string to a handler function. A URL pattern can be a string literal, or contain regular expressions or variable rules.

Due to the way Decanter matches requested URLs against URL patterns, you should always define your URL patterns in order from the most general to the most specific. So, for example, the "/(.*)" regular expression is at the start of defurls, while the "/hello" literal is at the end. Likewise, the first URL patterns added via add-mapping should be more general than the last.

Regular Expressions

Decanter uses the CL-PPCRE library for regular expressions. Review its documentation for details on the regular expression format and how to write them.

You can include regular expressions in a URL pattern by marking it with the :regex keyword, either in the defurls macro or when using the add-mapping method:

(defurls *urls*
    '("/(.*)" :regex placeholder))

(add-mapping *app* "/hello/(.*)" hello-name :regex t)

Regular expressions are used when matching a requested URL against a URL pattern. They can capture substrings, which are provided to handler functions in the pattern-matches parameter.

For example:

(defhandler placeholder
  (:get ()
        (format t "~a" pattern-matches)
        "Placeholder"))

Prints a plist of the form:

(:type :regex
 :match ...
 :substrings #(...))

Syntax Sugar

Decanter includes a read macro to make specifying regular expression URL patterns a bit cleaner. Since read macros defined by different systems can collide with each other, it is disabled by default.

The read macro works within defurls and must be enabled before calling defurls:

(enable-regex-sugar)

(defurls *urls*
    '(#?"/(.*)" hello))

This is equivalent to:

(defurls *urls*
    '("/(.*)" :regex hello))

Variable Rules

You can add variable rules to a URL pattern by marking them with (variable-name). Your handler function then takes the variable-name as an argument to its request method forms. Optionally, you can also specify a type for the variable rule like (variable-name :float), which will limit what the rule matches against.

Note that variable rules can perform type checking, but not type conversions. Their values are passed as strings to handler functions, which may perform the conversions later.

(url-regex-sugar)

(defurls *urls*
    '(#?"/(.*)" placeholder
      "/hello/(name)/" hello
      "/vector/(x :float)/(y :float)/(z :float)/" vec))

(defapp *app* *urls*)

(defhandler placeholder
  (:get () ""))

(defhandler hello
  (:get (name)
        (format nil "Hello, ~a!" name)))

(defhandler vec
  (:get (x y z)
        (format nil "<~a, ~a, ~a>" x y z)))

Valid types for variable rules are as follows:

:string Accepts strings with characters that are standard-char-p.
:integer Accepts positive integers with digits that are digit-char-p.
:float Accepts positive floats with digits that are digit-char-p.
:number Accepts either :integer or :float values.
:boolean Accepts strings: "t", "true", "1", "nil", "f", "false", "0" in either lowercase, uppercase, or some mix thereof.

Trailing Slashes

The following URL patterns differ in their use of a trailing slash:

(defurls *urls*
    '("/projects/" projects
      "/about" about))

The URL for the projects handler function has a trailing slash. If you access this URL without a trailing slash, Decanter raises an error or replies with a 404 "Not Found" error, depending on how the application is configured.

The URL for the about handler function does not have a trailing slash. Accessing this URL with a trailing slash causes Decanter to raise an error or reply with a 404 "Not Found" error, depending on how the application is configured.

We recommend sticking to a convention with your URLs: either always use trailing slashes or never use them. Conventions help keep websites clean and predictable.

URL Building

To build a URL to a specific handler, use the handler-url macro. It accepts an application and the name of a handler as its first arguments, and any number of subsequent keyword arguments, each corresponding to a variable rule in a URL pattern. Unknown keyword arguments are appended to the URL as query parameters.

When the pattern is instead a :regex pattern, the pattern string is returned with no modifications.

Why would you want to build URLs using the URL reversing macro handler-url, instead of hard-coding them into your code?

  1. Reversing is often more descriptive than hard-coding the URLs.
  2. You can change your URLs in one go instead of needing to remember to manually change hard-coded URLs.
  3. URL building handles escaping of special characters transparently.
  4. The generated paths are always absolute, avoiding unexpected behavior of relative paths in browsers.

For example:

(require :decanter)
(use-package :decanter)

(defurls *urls*
    '("/" index
      "/login/" login
      "/user/(username)/" user-profile
      "/print-handlers/" print-handlers))

(defapp *app* *urls*)

(defhandler index
  (:get () "index"))

(defhandler login
  (:get () "login"))

(defhandler user-profile
  (:get (username) (format nil "~a's profile" username)))

(defhandler print-handlers
  (:get ()
        (format t "~s~%" (handler-url index))
        (format t "~s~%" (handler-url login))
        (format t "~s~%" (handler-url login :next "/"))
        (format t "~s~%" (handler-url user-profile :username "John Doe"))
        "print-handlers"))

(run *app*)

(format t "~s~%" (handler-url *app* index))
(format t "~s~%" (handler-url *app* login))
(format t "~s~%" (handler-url *app* login :next "/"))
(format t "~s~%" (handler-url *app* user-profile :username "John Doe"))
"/"
"/login/"
"/login/?next=%2F"
"/user/John%20Doe/"

When using handler-url inside a defhandler body, you should omit the application parameter.

Static Files

Dynamic web applications also use static files such as images, CSS, and JavaScript.

Decanter is configured by default to serve static files starting at the URL /static/. It looks for these files in the ./static/ subdirectory of the directory in which your Lisp implementation was started. If you started your Lisp via Slime or some other process, that directory should be ~/, and static files will be served from ~/static/.

You can specify both the application's root directory and its static directory as pathnames when creating the application with the defapp macro:

(defapp *app* *urls*
  :application-directory #P"~/projects/decanter-app/"
  :static-directory #P"~/projects/decanter-app/static/")

Alternatively, you can change them after the application is created:

(setf (application-directory *app*)
      #P"~/projects/decanter-app/")

(setf (static-directory *app*)
      #P"~/projects/decanter-app/static/")

The static directory is often, but does not need to be, a subdirectory of the application's root directory.

To generate URLs for static files, use the static-url method:

(static-url *app* :filename "style.css")

(static-url *app* :path "css/style.css")

(static-url *app* :subpath "css/" :filename "style.css")

(static-url *app* :filename "style.css" :pathname t)

(static-url *app* :path "css/style.css" :pathname t)

(static-url *app*
            :subpath "css/"
            :filename "style.css"
            :pathname t)

Which, with the application's default static-url-prefix, return:

"/static/style.css"

"/static/css/style.css"

"/static/css/style.css"

#P"/static/style.css"

#P"/static/css/style.css"

#P"/static/css/style.css"

You can retrieve or set the application's current static URL prefix with:

(static-url-prefix *app*)

(setf (static-url-prefix *app*) "/static-content/")
"/static/"

"/static-content/"

When using static-url within a defhandler body, you do not need to specify the application, which is already known by the handler:

(defhandler hello
  (:get ()
        (format t "~s~%" (static-url "style.css"))
        "Hello, world!"))

Request Handlers

A request handler - or handler function - is a top level function that processes an HTTP request and returns either a string or a response object. Handler functions accept the following parameters:

application An application object.
request A request object.
pattern A string containing a URL pattern.
pattern-matches A list containing the results of matching the request target against the pattern, which occurs prior to calling the handler.
request-parameters-plist A plist of parameters from the HTTP request and pattern variable rules, if applicable. HTTP request parameters may come from a query string, or a form, etc.

While it is possible to create request handlers manually, we recommend using the defhandler macro, which provides many convenience features:

(defhandler hello
  (:get () "Hello, world!"))

HTTP Methods

Decanter can accept HTTP requests using any HTTP request method. Specifying how to handle different HTTP request methods is easy using the defhandler macro:

(defhandler hello
  (:get () "Hello, get!")
  (:head () "Hello, head!")
  (:post () "Hello, post!")
  (:put () "Hello, put!")
  (:delete () "Hello, delete!")
  (:connect () "Hello, connect!")
  (:options () "Hello, options!")
  (:trace () "Hello, trace!")
  (:patch () "Hello, patch!"))

Recall that a handler function is associated with a URL pattern. It is therefore straightforward to handle multiple different HTTP request methods on the same URL pattern.

A Lisp form within a defhandler declaration that is associated with an HTTP request method is called a subhandler:

(:get () "Hello, get!")

A subhandler consists of a keyword corresponding to an HTTP request method, a list of HTTP request parameters and URL variable rule values, and a body of Lisp forms. The body should always evaluate to either a string or an HTTP response object.

Capturing Variables

Subhandlers can capture HTTP request parameters and URL variable rule values as variables. In the event there is a name collision between these two categories, an error is either raised or a 400 "Bad Request" is returned to the client.

URL variable rule values are always captured. Only query string parameters are captured from GET, HEAD, DELETE, OPTIONS, and TRACE requests. Parameters specified in the request body are captured from POST, PUT, and PATCH requests (e.g. from a web form); if a query string is also present, it is captured with the name request-query-parameters. No parameters are captured from CONNECT requests.

(url-regex-sugar)

(defurls *urls*
    '(#?"/(.*)" hello
      "/add(.*)" :regex add
      "/vector/(x :float)/(y :float)/(z :float)/" vec))

(defapp *app* *urls*)

(defhandler hello
  (:get ((name :string))
        (when (not name)
          (setf name "World"))
        (format nil "Hello, ~a!" name)))

(defhandler add
  (:get ((n :integer :required) (m :integer :required))
        (format nil "~a" (+ n m))))

(defhandler vec
  (:get ((x :float :required)
         (y :float :required)
         (z :float :required))
        (format nil "<~a, ~a, ~a>" x y z)))

So that:

HTTP Request URL HTTP Response Body
/ "Hello, World!"
/?name=Decanter "Hello, Decanter!"
/add?n=1&m=2 "3"
/vector/1.1/2.2/3.3/ "<1.1, 2.2, 3.3>"

Subhandler variables are strings or nil by default.

(defhandler hello
  (:get (name)
        (when (not name)
          (setf name "World"))
        (format nil "Hello, ~a!" name)))

Adding a type specifier causes a subhandler variable, when it is not nil, to be parsed into that type.

(defhandler hello
  (:get ((name :string))
        (when (not name)
          (setf name "World"))
        (format nil "Hello, ~a!" name)))

(defhandler add
  (:get ((n :integer) (m :integer))
        (format nil "~a" (+ n m))))

You can mandate that certain subhandler variables are required. Values of nil are rejected.

(defhandler add
  (:get ((n :integer :required) (m :integer :required))
        (format nil "~a" (+ n m))))

Lastly, you may associate a predicate with a subhandler variable. When a predicate is specified along with a type, the predicate expects a variable of that type as its parameter. Otherwise, it expects a string parameter.

The predicate is called on the variable. If the predicate returns a non-nil value, the subhandler body is evaluated and an HTTP response is returned as normal. If the predicate returns nil, the body is not evaluated. An error is either raised or a 400 "Bad Request" response is returned to the client, depending on how the Decanter application is configured.

The use of predicates can help with server-side input validation.

(defun string-or-nil-p (var)
  (or (null var)
      (stringp var)))

(defhandler hello
  (:get ((name #'string-or-nil-p))
        (when (not name)
          (setf name "World"))
        (format nil "Hello, ~a!" name)))

(defun in-range-p (var)
  (and (>= var 0) (< var 10)))

(defhandler add
  (:get ((n :integer #'in-range-p :required)
         (m :integer #'in-range-p :required))
        (format nil "~a" (+ n m))))

Order matters. The following are valid subhandler variable lists:

()
(var)
((var :required))
((var :integer))
((var :integer :required))
((var :integer #'in-range-p))
((var :integer #'in-range-p :required))

Of course, more than one variable can be specified.

The Request Object

Request objects are instances of the CLOS class request, which is defined within Decanter.

A request object named request is always passed to and available within handler functions. When using defhandler to define handler functions, this is done automatically.

A request object has the following fields and accessors:


method

A keyword representing the HTTP request method.

Valid request methods are: :get, :head, :post, :put, :delete, :connect, :options, :trace, and :patch.

(request-method request)

Returns the HTTP request method from the object request as a keyword: :get, :post, etc.

(setf (request-method request) value)

Sets the HTTP request method on the object request to a keyword value.


target

A string containing the HTTP request target. When applicable, this may include a path, query, or fragment.

For example:

  • /
  • /index.html
  • /hello?name=person
  • /about#section
  • /person/?id=100#name
(target request)

Returns the HTTP request target from the object request as a string.

(setf (target request) value)

Sets the HTTP request target on the object request to a string value.


headers

A plist containing the HTTP request headers.

For example: (:content-type "text/plain" :content-length "100")

(headers request)

Returns the HTTP request headers from the object request as a plist.

(setf (headers request) value)

Sets the HTTP request headers on the object request to a plist value.


body

An octet vector containing the HTTP request body.

The vector has the type (simple-array (unsigned-byte 8) (length)), where length is the length of the vector.

(body request)

Returns the HTTP request body from the object request as an octet vector.

(body-string request)

Returns the HTTP request body from the object request as a string.

(setf (body request) value)

Sets the HTTP request body on the object request to an octet vector value.


cookies

An alist of string keys and string values containing the cookies sent by the client.

(cookies request)

Returns the cookies from the object request as an alist.

(setf (cookies request) value)

Sets the cookies on the object request to an alist value.

(get-cookie request key)

Gets a cookie value from the object request with the key.


clack-env

A plist containing the Clack environment.

Clack operates at a lower level of abstraction than Decanter, but there may be situations where access to the Clack environment is useful to a user.

(clack-env request)

Returns the Clack environment from the request body as a plist.


Matched Patterns

When a URL pattern matches against a requested URL, it collects some data. That data is provided to the handler function in the parameter pattern-matches. This parameter is a plist with a structure that depends on the type of URL pattern.

With a regular expression URL pattern:

(defurls *urls*
    '("/(.*)" :regex handler))

(defhandler handler
  (:get () (format nil "~s" pattern-matches)))

pattern-matches looks like the following, with :match and :substrings captured by the CL-PPCRE library function scan-to-strings:

(:type :regex
 :match ...
 :substrings #(...))

With a URL pattern containing variable rules:

(defurls *urls*
    '("/vector/(x :float)/(y :float)/(z :float)/" vec))

(defhandler vec
  (:get () (format nil "~s" pattern-matches)))

pattern-matches looks like the following:

(:type :var-rules
 :total-chars-matched ...
 :variables ((:name ...
              :type ...
              :value ...)
             ...))

A variable will not always have a :type:, but it will always have a :name and a :value.

File Uploads

You can handle file uploads with Decanter easily. When you're using an HTML form, make sure to set the enctype="multipart/form-data" attribute on that form, otherwise the browser will not transmit your files.

An uploaded file is captured as a variable in a handler function. A file variable is a list containing an input stream, a filename, and a file type; the stream holds the file's contents, while the filename and file type are strings.

Note that a filename is user-specified input, and you should neither trust its value nor make use of it without validation and cleaning.

Decanter provides two utility functions to make working with file streams easy:

  • file-to-string
  • file-to-vector

Each function reads the stream in a file variable, returning the file's contents as a string or an octet vector, respectively.

Here is an example showing how it all works. In this example, we echo a text file back to its uploader:

(require :decanter)
(use-package :decanter)

(defurls *urls*
    '("/" file))

(defapp *app* *urls*)

(defhandler file
  (:post (file)
         (destructuring-bind (file-stream file-name file-type)
             file
           (declare (ignore file-name file-type))
           (format nil "~a" (file-to-string file-stream)))))

(run *app*)

We'll send a text file using an HTTP client, Dexador. The text file is named file.txt and consists of one line: file contents.

(require :dexador)

(dex:post "http://localhost:5000"
          :content '((:file . #P"file.txt")))

Our file variable looks like this:

'(#<FLEXI-STREAMS::VECTOR-INPUT-STREAM {102F7377F3}>
  "file.txt"
  "text/plain")

The handler function returns this:

"file contents"

Instead, we can use file-to-vector:

(defhandler file
  (:post (file)
         (destructuring-bind (file-stream file-name file-type)
             file
           (declare (ignore file-name file-type))
           (format nil "~a" (file-to-vector file-stream)))))

In which case the handler function returns:

"#(102 105 108 101 32 99 111 110 116 101 110 116 115)"

Cookies

To access cookies you can use the get-cookies method on a request object. To set cookies you can use the set-cookie method on a response object. The cookies field of a request object is an alist with all of the cookies the client transmits.

If you want to use sessions, do not use the cookies directly, but instead use the Sessions in Decanter that add some security on top of cookies.

Reading cookies:

(defhandler index
  (:get ()
        (format nil "Hello, ~a!"
                (get-cookie request "username"))))

Storing cookies:

(defhandler index
  (:get ()
        (let ((response (response-200)))
          (set-cookie response
                      "username"
                      :value "the username")
          response)))

Note that cookies are set on response objects, which can be returned from handler functions just like strings.

set-cookie takes the following parameters:

(set-cookie (response key
                      &key
                      (value "")
                      expires
                      (path "/")
                      domain
                      secure
                      httponly
                      samesite))
  • response (response) - the response object where we are setting the cookie.
  • key (string) - the key (name) of the cookie to be set.
  • value (string) - the value of the cookie.
  • expires (universal time or nil) - when the cookie expires.
  • path (string) - limits the cookie to a given path, per default it will span the whole domain.
  • domain (string or nil) - if you want to set a cross-domain cookie. For example, "example.com" will set a cookie that is readable by the domain www.example.com, foo.example.com, etc. Otherwise, a cookie will only be readable by the domain that set it.
  • secure (t or nil) - if t, the cookie will only be available via HTTPS.
  • httponly (t or nil) - disallow JavaScript access to the cookie.
  • samesite (:lax, :strict, or nil) - limit the scope of the cookie to only be attached to requests that are "same-site".

HTML Templates

HTML generation is provided by Spinneret. Decanter provides the with-html-string macro that wraps some of Spinneret's functionality.

Decanter does not ship with a conventional HTML template engine. Instead, it's easy to create templates within Lisp itself by using defmacro, defun, and with-html-string. HTML is written using s-expressions.

This keeps the full power of Lisp at your fingertips, rather than forcing you to use a restrictive language provided by an HTML template engine. Remain disciplined and keep your application logic separate from template functions and macros.

For example:

(require :decanter)
(use-package :decanter)

(defurls *urls*
    '("/" index
      "/vector/(x :float)/(y :float)/(z :float)/" vec))

(defapp *app* *urls*)

(defmacro with-page (&key (title "Decanter") body)
  `(with-html-string
     (:doctype)
     (:html
      (:head
       (:title ,title))
      (:body ,@body))))

(defun index-html ()
  (with-page
    :title "Decanter - Index"))

(defun vec-html (&key v0 v1 v2)
  (with-page
    :title "Decanter - Vector"
    :body ((:p (format nil "<~a, ~a, ~a>" v0 v1 v2)))))

(defhandler index
  (:get () (index-html)))

(defhandler vec
  (:get ((x :float :required)
         (y :float :required)
         (z :float :required))
        (vec-html :v0 x :v1 y :v2 z)))

(run *app*)

It is possible to use a template engine if desired, however.

You would render a template to a string within a handler function, then return either the rendered template string or a response object containing the rendered template string as its body.

Responses

Handler functions ultimately return response objects, which are used in Decanter's internals to create HTTP responses.

The return value from a handler function is automatically converted to a response object according to this process:

  1. If the return value is a string it is converted to a response object with the string as the response body, a 200 status code, and Content-Type: text/html.
  2. If it's a list, a JSON response is created from that list using cl-json.
  3. If the return value is already a response object, no conversion is performed.

Sometimes you will want to construct your own response objects. Decanter provides many functions to do so:

(response-200 &optional headers body cookies)

Returns a new response object with a 200 status code, as well as the headers, body, and cookies, if specified.

The headers parameter is a plist, e.g. (:content-type "text/plain"), the body parameter is a string, and the cookies parameter is an alist keyed by strings, with plist values containing fields equivalent to the keyword parameters of the set-cookie function.

Other convenience functions include:

(response-text body)

Returns a new response object with a 200 status code, the content-type header specified as "text/plain", and the specified body, which is a string.

(response-html body)

Returns a new response object with a 200 status code, the content-type header specified as "text/html", and the specified body, which is a string containing HTML.

(response-css body)

Returns a new response object with a 200 status code, the content-type header specified as "text/css", and the specified body, which is a string containing CSS.

(response-js body)

Returns a new response object with a 200 status code, the content-type header specified as "text/javascript", and the specified body, which is a string containing JavaScript.

Redirects

Redirecting a request is as simple as returning a response object from the handler function with a 303 status code and an appropriate location header. Decanter provides constructors that make doing so easy:

(response-303 location)

Returns a new response object with a 303 status code and the location header with the specified location, which is a string containing a URL.

(response-redirect location)

An alias for response-303. It behaves identically.

Errors

Returning an HTTP error is straightforward and involves returning a response object from the handler function with an appropriate status code. Decanter provides constructors that make doing so easy:

(response-400 &optional headers body)

Returns a new response object with a 400 status code, the content-type header set to "text/plain" and the body set to "Bad Request".

The user may optionally specify different headers and a different body.

(response-404 &optional headers body)

Returns a new response object with a 404 status code, the content-type header set to "text/plain" and the body set to "Not Found".

The user may optionally specify different headers and a different body.

(response-500 &optional headers body)

Returns a new response object with a 500 status code, the content-type header set to "text/plain" and the body set to "Internal Server Error".

The user may optionally specify different headers and a different body.

(response-bad-request &optional headers body)

An alias for response-400. It behaves identically.

(response-not-found &optional headers body)

An alias for response-404. It behaves identically.

(response-error &optional headers body)

An alias for response-500. It behaves identically.

APIs with JSON

JSON is a common response format when writing an API. Decanter allows you to write this type of API easily. If you return a list from a handler function, it will be converted to a JSON response.

(require :decanter)
(use-package :decanter)

(defurls *urls*
    '("/json/" json))

(defapp *app* *urls*)

(defhandler json
  (:get ()
        '(:array

          (:object :name "user 1" :profile-image "image-1.jpg")

          (:object ("name" . "user 2") ("profile-image" . "image-2.jpg"))

          (:array 1 2 3)

          #(1 2 3)

          t

          nil

          "hello"

          :true

          :false

          :null)))

(run *app*)
[
    {
        "name": "user 1",
        "profileImage": "image-1.jpg"
    },
    {
        "name": "user 2",
        "profile-image": "image-2.jpg"
    },
    [
        1,
        2,
        3
    ],
    [
        1,
        2,
        3
    ],
    true,
    null,
    "hello",
    "true",
    "false",
    "null"
]
(defhandler json
  (:get ()
        '(:object
          :name "user 1"
          :profile-image "image-1.jpg")))
{
    "name": "user 1",
    "profileImage": "image-1.jpg"
}

Decanter will convert any supported data type to JSON, which means that any data in the list must be convertible by cl-json.

Note that a list can be converted to either an :array or an :object in JSON, depending on what keyword you specify as the first element in the list. When working with lists, you must always specify a keyword.

Both association lists and property lists may be converted to json objects.

Sessions

Storage of session information, or other data one might want to associate with a client but not allow that client to modify, should be done server side.

Decanter can expose a session on the request object. The session persists from one request to the next. Each session is represented on the client by a cookie containing a session id, but no other information.

A session provides a hash table that can be used to store data across requests. However, this storage is volatile and exists in memory; an outage, crash, or similar event will wipe it. It's therefore recommended that you implement session persistence in a database.

Enable sessions when calling the run function:

(run *app* :session t)

Here is how sessions work:

(require :decanter)
(use-package :decanter)

(defurls *urls*
    '("/" index
      "/login/" login
      "/logout/" logout))

(defapp *app* *urls*)

(defhandler index
  (:get ()
        (if (session "username")
            (format nil "Logged in as ~a" (session "username"))
            "You are not logged in")))

(defhandler login
  (:get ()
        (with-html-string
          (:form :method "post"
                 (:p (:input :type "text" :name "username"))
                 (:p (:input :type "submit" :value "Log In")))))
  (:post (username)
         (set-session "username" username)
         (response-redirect (handler-url index))))

(defhandler logout
  (:get ()
        (rem-session "username")
        (response-redirect (handler-url index))))

(run *app* :session t)

Interface

The following functions help you work with sessions:


(session-id request)

Returns the session id, which is a string.


(session-options request)

Returns a plist of session options, e.g.

(:ID "..."
 :NEW-SESSION T
 :CHANGE-ID NIL
 :EXPIRE NIL)

(session-table request)

Returns the hash table associated with the session, which can be used to store data across requests.


(session request key)

Returns a value from the session hash table, keyed by the string key.


(set-session request key value)

Sets a value in the session hash table, keyed by the string key.


(rem-session request key)

Removes a value from the session hash table, keyed by the string key.


(clear-session request)

Deletes all keys and values from the session hash table.


When calling these functions within the body of a defhandler, omit the request parameter.

For example:

(defhandler session-id
  (:get () (session-id)))

Middleware

Decanter is based on Clack, which provides the ability for middleware to wrap an application environment and affect HTTP requests and responses.

A Decanter application always uses static middleware to support serving static files. Other middleware may be specified in the keyword parameter middleware, which is expected to be a list, when calling the run function on an application object.

For example:

(require :decanter)
(use-package :decanter)

(defurls *urls*
    '("/" hello))

(defapp *app* *urls*)

(defhandler hello
  (:get () "Hello, world!"))

(run *app* :middleware (list *middleware-session*))

Enables session tracking and exposes session data in the headers of a request object.

*middleware-session* is a convenience alias in Decanter. You could also enable the session middleware like this:

(run *app*
     :middleware (list lack/middleware/session:*lack-middleware-session*))

Lack is the core of Clack and contains other middleware that may be useful.

Logging

Writing to a log is easy with Decanter. You'll need to enable logging when running an application.

By default, Decanter writes to the decanter.log file in the directory where your Lisp implementation was started. You may change this by setting the *log-file-name* parameter in the Decanter package to a pathname.

For example:

(require :decanter)
(use-package :decanter)

(setf *log-file-name* #P"decanter-log.log")

(defurls *urls*
    '("/" hello))

(defapp *app* *urls*)

(defhandler hello
  (:get () "Hello, world!"))

(run *app* :logging t)

(log-debug *app* "A value for debugging.")
(log-info *app* "A value for info.")
(log-warning *app* "A value for a warning: ~s" 42)
(log-error *app* "A value for an error.")

The four log functions, shown above, behave like format. They accept an application object, a format string, and an arbitrary number of value that are spliced into that format string. They do not, however, take a stream as a parameter.

Deploying

Decanter uses the Woo web server. Woo is fairly fast, but it's still a dynamic application server.

We therefore recommend running Decanter applications behind a reverse proxy, either Apache or Nginx. It can also be a good idea to enable caching on the reverse proxy server for static assets and pages that are either static or rarely change.

Decanter applications benefit from the ability of Lisp implementations, such as SBCL, to be executed in shell scripts or produce standalone binaries. Assuming that you're using SBCL, these are both good ways to package a Decanter application.

Note that you will need libev installed to run Woo. libev is available from most Linux distribution package managers, and on Homebrew.

Running and Application Settings

In either case, when you call the run function, you must enable standalone functionality:

(run *app* :standalone t)

You will almost certainly want to enable production mode, which alters error handling behavior to send HTTP responses with error codes instead of raising Lisp errors that can pause or terminate execution of the application:

(defapp *app *urls* :production t)

(run *app* :standalone t)

Or:

(setf (production *app*) t)

(run *app* :standalone t)

You may also wish to disable the printing of debug messages:

(defapp *app *urls*
  :production t
  :debug nil)

(run *app* :standalone t)

Or:

(setf (production *app*) t
      (application-debug *app*) nil)

(run *app* :standalone t)

Deploying a Shell Script

Note that deployment of a shell script means that SBCL will need to be installed on the target system, along with all of your application's dependencies. While the Common Lisp ecosystem is generally fairly stable, this may not be ideal.

Create a shell script:

#!/usr/bin/env -S sbcl --script
(load "~/.sbclrc")

(require :decanter)
(use-package :decanter)

(defurls *urls*
    '("/" hello))

(defapp *app* *urls*
  :production t
  :debug nil)

(defhandler hello
  (:get () "Hello, world!"))

(run *app* :standalone t)

Deploying a Binary

Creating a standalone binary can load all of an application's dependencies into a Lisp image, which is then immediately saved in an executable format. Therefore, packaging an application in this way requires no dependencies to be installed on the target system, other than those external to Lisp itself, e.g. a database system.

Create a standalone binary:

(require :decanter)
(use-package :decanter)

(defurls *urls*
    '("/" hello))

(defapp *app* *urls*
  :production t
  :debug nil)

(defhandler hello
  (:get () "Hello, world!"))

(defun executable-entry-point ()
  (run *app* :standalone t))

Save the above in a file, e.g. hello.lisp. Then tell SBCL to load it and build a binary.

$ sbcl \
  --noinform --non-interactive \
  --eval "(load \"hello.lisp\")" \
  --eval "(sb-ext:save-lisp-and-die #p\"hello\" :toplevel #'executable-entry-point :executable t)"

You can run the binary like any other:

$ chmod +x hello
$ ./hello

A shell script or binary may then be automatically started by the operating system, for example by a systemd service, or otherwise deployed as you prefer.

The various Lisp implementations offer different types of functionality in this vein. Check your implementation's documentation to get a better sense of what features it makes available.

Patterns for Decanter

Decanter makes it easy to implement tasks and design patterns common to many web applications.

Structuring Applications

As your application grows, you may wish to split it between several files or directories. An application with this kind of structure is best organized into an ASDF system.

An ASDF system tells a Lisp implementation how Lisp packages are composed from files, so that the implementation can make those packages available to the user. A system can provide more than one package, and makes it convenient to do other things like bundling test suites.

ASDF is bundled with most Lisp implementations, so chances are you won't need to install it yourself. Like many things in the Lisp ecosystem, ASDF is powerful but complex. Here we aim to provide a streamlined introduction with an example application structure.

You can find code for the example here. It may be useful to examine as you follow along.

Note that while much of the code in this documentation employs use-package, this is considered poor style for large systems, since it can lead to collisions between symbols. The code for this example application does not do this; you can use it as a reference for a more robust style.

Example Application

Consider an application with the following structure:

/application
    /src
        main.lisp
        models.lisp
        views.lisp
        templates.lisp
    /tests
        tests.lisp
    /static
        style.css
    /templates
        page.lisp
        index.lisp
        login.lisp
        logout.lisp
        ...

Each file in the application/src directory defines a package:

application/src/main.lisp      => :application
application/src/models.lisp    => :application.models
application/src/views.lisp     => :application.views
application/src/templates.lisp => :application.templates

models.lisp contains functions that interact with a database. Functions exported by this package are used in views.lisp, where defhandler defines multiple handler functions. views.lisp also makes use of templates, which are just functions wrapping Spinneret markup, that return HTML strings. Templates are exported in templates.lisp.

main.lisp contains the application entry point, URL definitions in defurl, and defapp. Since URLs map to handler functions, it depends on views.lisp.

Individual templates reside inside files within the application/src/templates/ directory, but they are still part of the templates package, with (in-package :application.templates) as their first form. templates.lisp just exports each of these templates.

This pattern of splitting templates into their own files and exporting them using a thin templates.lisp file can make it easier to keep an application organized. The same is true for splitting models.lisp and views.lisp into their own directories and files, though for this example we haven't done this.

ASDF System

We'll make an ASD file for this system, which will make all four of its constituent packages available:

(defsystem "application"
  :description "An example Decanter application."
  :version "0.1.0"
  :author "Application Authors"
  :license "BSD-2"
  :depends-on ("decanter"
               "decanter.ext.db")
  :components (;; Templates

               (:file "application.templates"
                :pathname "src/templates")

               (:file "application.templates.page"
                :pathname "templates/page"
                :depends-on ("application.templates"))

               (:file "application.templates.index"
                :pathname "templates/index"
                :depends-on ("application.templates"))

               (:file "application.templates.login"
                :pathname "templates/login"
                :depends-on ("application.templates"))

               (:file "application.templates.logout"
                :pathname "templates/logout"
                :depends-on ("application.templates"))

               ;; Models

               (:file "application.models"
                :pathname "src/models")

               ;; Views

               (:file "application.views"
                :pathname "src/views"
                :depends-on ("application.templates"
                             "application.models"))

               ;; Application

               (:file "application"
                :pathname "src/main"
                :depends-on ("application.views"))))

The application depends on decanter and, since it makes use of a database, we have also added decanter.ext.db as a dependency.

After saving this file as application.asd, our application structure looks like this:

/application
    application.asd
    /src
        main.lisp
        models.lisp
        views.lisp
        templates.lisp
    /tests
        tests.lisp
    /static
        style.css
    /templates
        page.lisp
        index.lisp
        login.lisp
        logout.lisp
        ...

Your Lisp implementation needs to know where to find the system. You can achieve this by creating a symbolic link in one of two places: the Lisp system source path, or Quicklisp's local projects directory.

For example, on Linux:

$ ln -s ~/application ~/.local/share/common-lisp/source/
$ ln -s ~/application ~/quicklisp/local-projects/

Other Assets

Remember that static files are loaded from the file system by Decanter's internals, so we don't need to include them as part of the system definition. We do, however, need to make sure to set the right application directory and static directory in defapp, e.g.

(defapp *app* *urls*
  :application-directory #P"~/application/"
  :static-directory #P"~/application/static/")

Alternatively, you can specify paths relative to your ASDF system's root, which is where application.asd resides:

(defparameter *system-dir*
  (asdf:system-source-directory :application))

(defparameter *static-dir*
  (asdf:system-relative-pathname :application "static/"))

(defapp *app* *urls*
  :application-directory *system-dir*
  :static-directory *static-dir*)

Or relative to the current working directory using UIOP, which is part of ASDF:

(defparameter *system-dir* (uiop/os:getcwd))
(defparameter *static-dir* (merge-pathnames #P"static/" *system-dir*))

(defapp *app* *urls*
  :application-directory *system-dir*
  :static-directory *static-dir*)

In that case, remember to start your Lisp implementation in your application's directory, or set the current working directory once the Lisp is loaded, like this:

(uiop:chdir "~/application/")

Loading the System

With Quicklisp:

(ql:quickload :application)

With ASDF:

(asdf:load-system :application)

Sometimes you might want to force a complete rebuild of the system. You can do this with the :force option:

(asdf:load-system :application :force t)

Then, you can require the application's packages:

(require :application)
(require :application.models)
(require :application.views)
(require :application.templates)

Testing the System

Let's set up the ASD file to run some tests:

(defsystem "application"
  :description "An example Decanter application."
  :version "0.1.0"
  :author "Application Authors"
  :license "BSD-2"
  :depends-on ("decanter"
               "decanter.ext.db")
  :components (;; Templates

               (:file "application.templates"
                :pathname "src/templates")

               (:file "application.templates.page"
                :pathname "templates/page"
                :depends-on ("application.templates"))

               (:file "application.templates.index"
                :pathname "templates/index"
                :depends-on ("application.templates"))

               (:file "application.templates.login"
                :pathname "templates/login"
                :depends-on ("application.templates"))

               (:file "application.templates.logout"
                :pathname "templates/logout"
                :depends-on ("application.templates"))

               ;; Models

               (:file "application.models"
                :pathname "src/models")

               ;; Views

               (:file "application.views"
                :pathname "src/views"
                :depends-on ("application.templates"
                             "application.models"))

               ;; Application

               (:file "application"
                :pathname "src/main"
                :depends-on ("application.views")))
  :in-order-to ((test-op (test-op "application.tests"))))

(defsystem "application.tests"
  :description "Tests for an example Decanter application."
  :version "0.1.0"
  :author "Application Authors"
  :license "BSD-2"
  :depends-on ("application")
  :components ((:file "tests" :pathname "tests/tests"))
  :perform (test-op (op c) (symbol-call :application.tests :test)))

The ASD file now defines two systems: application and application.tests.

application specifies :in-order-to, which allows you to run tests on this system.

application.tests specifies :perform, which defines how to go about the testing. In this case, by calling the function test exported by the :application.tests package.

You can run tests like this:

(asdf:test-system :application)

Deploying as a Shell Script

Assuming you've defined an entry point that runs the application in standalone mode, e.g.

(defun executable-entry-point ()
  (decanter:run *app* :standalone t))

A shell script might look like this:

#!/usr/bin/env -S sbcl --script
(load "~/.sbclrc")

(require :application)

(application:executable-entry-point)

Deploying as a Binary

Assuming you've defined an entry point that runs the application in standalone mode, e.g.

(defun executable-entry-point ()
  (decanter:run *app* :standalone t))

Building a binary might look like this:

$ sbcl \
  --noinform --non-interactive \
  --eval "(asdf:load-system :application :force t)" \
  --eval "(require :application)" \
  --eval "(sb-ext:save-lisp-and-die #p\"application\" :toplevel #'application:executable-entry-point :executable t)"

You can run the binary like any other:

$ chmod +x application
$ ./application

Using SQLite3 with Decanter

SQLite3 is one of the most popular open source databases. You can easily use it in a Decanter application.

Here is a simple example of using SQLite3 with Decanter:

(require :decanter)
(require :decanter.ext.db)
(use-package :decanter)
(use-package :decanter.ext.db)

(defparameter *db-path* ":memory:")
(defparameter *db* (connect :sqlite3 :database-name *db-path*))

(statement *db*
           "CREATE TABLE users (
                id INTEGER PRIMARY KEY,
                user_name TEXT NOT NULL,
                age INTEGER NULL
            )")
(statement *db*
           "INSERT INTO users (
                user_name,
                age
            ) VALUES (?, ?)"
           "User Name" 25)

(defurls *urls*
    '("/" index))

(defapp *app* *urls*)

(defhandler index
  (:get () (format nil "~s" (query *db* "SELECT * FROM users"))))

(run *app*)

Navigating to http://localhost:5000 shows this:

((:ID 1 :USER-NAME "User Name" :AGE 25))

You may want to shut down the application. Remember to disconnect the database afterward:

(stop *app*)
(disconnect *db*)

Note that we specified ":memory:" for the database path. SQLite3 allows the entire database to exist in RAM. Usually you will want to specify a path on the file system such as "./test.db"

Returning JSON

You might write a function to convert database output to JSON:

(defun json (db-rows)
  (append (list :array)
          (mapcar (lambda (row)
                    (append (list :object) row))
                  db-rows)))

(defhandler index
  (:get () (json (query *db* "SELECT * FROM users"))))

Navigating to http://localhost:5000 shows this:

[
    {
        "id": 1,
        "userName": "User Name",
        "age": 25
    }
]

Database Transactions

A transaction is a group of statements or queries that are executed atomically; it is either executed entirely, or not at all. Transactions are useful when performing complex, multi-step operations on a database that could leave the database inconsistent if they were interrupted or only partially completed.

Decanter makes it easy to perform database transactions. Simply place multiple statements or queries inside with-transaction:

(with-transaction *db*
  (statement *db*
             "CREATE TABLE users (
                  id INTEGER PRIMARY KEY,
                  user_name TEXT NOT NULL,
                  age INTEGER NULL
              )")
  (statement *db*
             "INSERT INTO users (
                  user_name,
                  age
              ) VALUES (?, ?)"
             "User Name" 25))

Initial Schemas

Applications often ship a schema.sql file that creates a relational database. You can provide a function that creates a database based on such a schema.

For example, assuming a database connection *db*:

(defun init-db ()
  (let* ((schema-str (alexandria:read-file-into-string #P"schema.sql"))
         (schema-statements (split-sequence:split-sequence #\; schema-str)))
    (with-transaction *db*
      (mapcar (lambda (statement)
                (statement *db* statement))
              schema-statements))))

This example uses the Alexandria and split-sequence libraries.

It reads the schema file into a string, then splits that string by the semicolon delimiter into different SQL statements. Then, it executes each of those statements within a transaction.

The function isn't particularly robust and won't handle comments in a SQL file.

Generating SQL

Working with SQL strings is not always desirable. Sometimes, you might want to represent SQL using Lisp data structures, so that you could manipulate it using the full power of Lisp, rather than just string manipulation functions.

The SXQL library allows you to do this. Decanter's :decanter.ext.db package is designed to work with SXQL as seamlessly as SQL strings.

We can rewrite our example application to use SXQL:

(require :decanter)
(require :decanter.ext.db)
(require :sxql)
(use-package :decanter)
(use-package :decanter.ext.db)
(use-package :sxql)

(defparameter *db-path* ":memory:")
(defparameter *db* (connect :sqlite3 :database-name *db-path*))

(statement *db*
           (create-table :users
               ((id :type 'integer
                    :primary-key t)
                (user_name :type 'string
                           :not-null t)
                (age :type 'integer
                     :default :null))))
(statement *db*
           (insert-into :users
             (set= :user_name "User Name"
                   :age 25)))

(defurls *urls*
    '("/" index))

(defapp *app* *urls*)

(defhandler index
  (:get () (format nil "~s" (query *db* (select :* (from :users))))))

(run *app*)

SQL Injection Attacks

Preventing SQL injection attacks is a common concern when developing web applications. Typically, SQL injection occurs when building SQL queries by concatenating strings and variables that have not been sanitized.

Decanter cleans and escapes variables automatically when binding them to SQL queries using the statement and query functions, so you don't need to worry about doing so manually.