routes
2025-06-22
No Description
40ants-routes - Framework agnostic URL routing library
40ANTS-ROUTES ASDF System Details
- Description: Framework agnostic
URL
routing library. - Licence: Unlicense
- Author: Alexander Artemenko svetlyak.40wt@gmail.com
- Homepage: https://40ants.com/routes
- Bug tracker: https://github.com/40ants/routes/issues
- Source control: GIT
- Depends on: alexandria, cl-ppcre, serapeum, split-sequence, str
Overview
40ants-routes is a framework-agnostic URL
routing library for Common Lisp, inspired by Django's URL
routing system. It provides a clean and flexible way to define URL
routes, generate URL
s, and handle URL
parameters.
Features
- Defining routes with namespaces.
- Including routes from libraries into applications.
- Matching
URL
while extracting parameters from it. - Generating
URL
s based on route names. - Generating breadcrumbs.
Installation
(ql:quickload :40ants-routes)
Usage Examples
Defining Routes
Routes can be defined using the 40ants-routes/defroutes:defroutes
macro.
Inside it's body, use 40ants-routes/defroutes:get
, 40ants-routes/defroutes:post
, macro
to define final routes in the collection.
(uiop:define-package #:test-routes
(:use #:cl)
(:shadowing-import-from #:40ants-routes/defroutes
#:defroutes
#:include
#:get
#:post)
(:import-from #:40ants-routes/route-url
#:route-url)
(:import-from #:40ants-routes/handler
#:call-handler)
(:import-from #:40ants-routes/with-url
#:with-partially-matched-url
#:with-url))
(in-package #:test-routes)
(defroutes (*blog-routes* :namespace "blog")
(get ("/" :name "index")
(format t "Handler for blog index was called."))
(get ("/<string:slug>" :name "post")
(format t "Handler for blog post ~S was called."
slug)))
Routes, defined by this 40ants-routes/defroutes:defroutes
are stored in *blog-routes*
variable
and can be used either to 40ants-routes/defroutes:include
these routes into the route hierarchy,
or to search a route, matched to the URL
. See section Matching the URL
.
Here's an example demonstrating how to use an integer URL
parameter:
(defroutes (*article-routes* :namespace "articles") (get ("/" :name "index") (format t "Handler for articles index was called.")) (get ("/<int:id>" :name "article") (format t "Handler for article with ID ~D was called." id)))
In this example, the route will match URL
s like /123
and the argument ID
will be parsed as an integer.
You can also capture the rest of the URL
as a parameter using the .*
regex pattern:
(defroutes (*file-routes* :namespace "files") (get ("/" :name "index") (format t "Handler for files index was called.")) (get ("/<.*:path>" :name "file") (format t "Handler for file at path ~S was called." path)))
This will match URL
s like /documents/reports/annual/2023.pdf
and capture the entire path
documents/reports/annual/2023.pdf
as the PATH
argument.
Including Routes
Routes from libraries can be included in application routes using
40ants-routes/defroutes:include
function.
This way they can form a hyerarchy:
(defroutes (*app-routes* :namespace "app") (get ("/" :name "index") (format t "Handler for application's index page.")) (include *blog-routes* :path "/blog/"))
In it's turn, *blog-routes*
might also include other routes itself.
This allows to build a composable web-applications and libraries. For example, some library might build routes to show the list of objects, show details about an object, edit it and delete. Then such routes can be included into a more complex application.
Matching the URL
Imagine, user have opened the URL
with a path like this /blog/some-post
.
Then in your web-application you might setup the context in which this route
processing should happen. Use 40ants-routes/with-url:with-url
or 40ants-routes/with-url:with-partially-matched-url
macros to setup the context. Inside the context you can use call-handler
function to call
a body of the route, matched to the URL
:
TEST-ROUTES> (with-url (*app-routes* "/blog/some-post") (call-handler)) Handler for blog post "some-post" was called. TEST-ROUTES> (with-url (*app-routes* "/blog/") (call-handler)) Handler for blog index was called. TEST-ROUTES> (with-url (*app-routes* "/") (call-handler)) Handler for application's index page.
40ants-routes/with-url:with-url
will signal 40ants-routes/errors:no-route-for-url-error
error if there is no route matching the whole URL
, but 40ants-routes/with-url:with-partially-matched-url
will
try to do the best it can.
So, inside the 40ants-routes/with-url:with-url
body you can use call-handler
always, while inside the 40ants-routes/with-url:with-partially-matched-url
macro handler should be called only if
40ants-routes/route:current-route-p
function returns T.
Generating URLs
Another feature of 40ants-routes
is URL
generation.
URL
s can be generated using the 40ants-routes/route-url:route-url
function. Like
call-handler
, it should be called when URL
context is available.
In our application routes tree there are two index
routes, but we can get paths to both of them
using namespaces. Route's namespace is defined as a list of names from the root route, given
to the with-url
macro up to the matched route. Each defroutes
form or a call to include
form
create an object having the name. These names are added to the current route's namespace.
Imagine we are on the blog-post page and we want to get path to all blog posts. Easiest way
to do this, is to call route-url
function with only route name:
TEST-ROUTES> (with-url (*app-routes* "/blog/some-post") (route-url "index")) "/blog/"
But this will not work if the user is on the root page:
TEST-ROUTES> (with-url (*app-routes* "/") (route-url "index")) "/"
You might want to make URL
resolution more stable, especially if these URL
s are used in some common
page parts such as header or footer. In this case, help URL
resolver by giving it a namespace:
TEST-ROUTES> (with-url (*app-routes* "/") (route-url "index" :namespace '("app" "blog"))) "/blog/"
Note, when you are building a reusable component which creates it's own 40ants-routes/routes:routes
(1
2
)
object, you should not use these absolute namespaces, because you don't know beforehand which namespace
will be used by user when including the component's routes.
Let's update our blog component routes and add one to edit the blog post:
TEST-ROUTES> (defroutes (*blog-routes* :namespace "blog") (get ("/" :name "index") (format t "Handler for blog index was called.")) (get ("/<string:slug>" :name "post") (format t "Handler for blog post ~S was called.~ To edit post go to ~S." slug (route-url "edit-post" :slug slug))) (get ("/<string:slug>/edit" :name "edit-post") (format t "Handler for blog post ~S edit form was called." slug))) #<40ANTS-ROUTES/ROUTES:ROUTES "blog" 3 subroutes>
Note, how we did use route-url
inside the /<string:slug>
handler to get
path to the post edit page.
Now, let's try to call this handler when this blog's routes are included into the application routes:
TEST-ROUTES> (with-url (*app-routes* "/blog/some-post") (call-handler)) Handler for blog post "some-post" was called.To edit post go to "/blog/some-post/edit".
See, it did return /blog/some-post/edit
path to the edit page and there wasn't need to specify
a namespace at all!
Generating Breadcrumbs
Breadcrumbs can be generated using the 40ants-routes/breadcrumbs:get-breadcrumbs
function. This function returns a list of 40ants-routes/breadcrumbs:breadcrumb
objects that represent the path from the root to the current page.
Each 40ants-routes/breadcrumbs:breadcrumb
object has the following properties:
- The
URL
path to the breadcrumb (accessible via40ants-routes/breadcrumbs:breadcrumb-path
) - The display title for the breadcrumb (accessible via
40ants-routes/breadcrumbs:breadcrumb-title
) - The route object associated with the breadcrumb (accessible via
40ants-routes/breadcrumbs:breadcrumb-route
)
To use breadcrumbs, you need to define routes with titles:
(defroutes (*admin-users-routes* :namespace "users") (post ("/" :name "users" :title "Users") (format nil "Users list")) (get ("/<string:username>" :name "user" :title "User Profile") (format nil "User profile: ~A" username))) (defroutes (*admin-routes* :namespace "admin") (get ("/" :name "admin-index" :title "Admin") (format nil "Admin index")) (include *admin-users-routes* :path "/users/")) (defroutes (*app-routes* :namespace "app") (get ("/" :name "index" :title "Home") (format nil "App index")) (include *admin-routes* :path "/admin/"))
Then, you can generate breadcrumbs for a specific URL
:
TEST-ROUTES> (with-url (*app-routes* "/admin/users/john") (let ((crumbs (40ants-routes/breadcrumbs:get-breadcrumbs))) ;; This way you can get all paths or titles: (values (mapcar #'40ants-routes/breadcrumbs:breadcrumb-path crumbs) (mapcar #'40ants-routes/breadcrumbs:breadcrumb-title crumbs)))) ("/" "/admin/" "/admin/users/" "/admin/users/john") ("Home" "Admin" "Users" "User Profile")
or to generate an HTML
code like this:
TEST-ROUTES> (with-url (*app-routes* "/admin/users/john") (let ((crumbs (40ants-routes/breadcrumbs:get-breadcrumbs))) (format t "<nav aria-label=\"breadcrumb\">~%") (format t " <ol class=\"breadcrumb\">~%") (loop for crumb in crumbs for last-p = (eq crumb (car (last crumbs))) do (format t " <li class=\"breadcrumb-item~:[~; active~]\"~:[~; aria-current=\"page\"~]>~%" last-p last-p) (if last-p (format t " ~A~%" (40ants-routes/breadcrumbs:breadcrumb-title crumb)) (format t " <a href=\"~A\">~A</a>~%" (40ants-routes/breadcrumbs:breadcrumb-path crumb) (40ants-routes/breadcrumbs:breadcrumb-title crumb))) (format t " </li>~%")) (format t " </ol>~%") (format t "</nav>~%"))) <nav aria-label="breadcrumb"> <ol class="breadcrumb"> <li class="breadcrumb-item"> <a href="/">Home</a> </li> <li class="breadcrumb-item"> <a href="/admin/">Admin</a> </li> <li class="breadcrumb-item"> <a href="/admin/users/">Users</a> </li> <li class="breadcrumb-item active" aria-current="page"> User Profile </li> </ol> </nav>
For more advanced usage, you can also use functions as route titles to generate dynamic titles based on URL
parameters. This is demonstrated in the test file:
First, you need to define a function which will accept an arguments extracted from URL
:
(defun get-user-name (&key username &allow-other-keys) "A function for retrieving user display names based on username parameter" (cond ((string= username "john") "John Smith") ((string= username "jane") "Jane Doe") (t (format nil "User: ~A" username))))
Then redefine routes, to use this function as TITLE
argument of the route:
(defroutes (*admin-users-routes* :namespace "users")
(post ("/" :name "users" :title "Users")
(format nil "Users list"))
(get ("/<string:username>"
:name "user"
;; Example of using a function for retrieving
;; route title dynamically at runtime:
:title #'get-user-name)
(format nil "User profile: ~A" username)))
And now you will get a real user's name as the last breadcrumb title:
TEST-ROUTES> (with-url (*app-routes* "/admin/users/john") (let ((crumbs (40ants-routes/breadcrumbs:get-breadcrumbs))) (values (mapcar #'40ants-routes/breadcrumbs:breadcrumb-path crumbs) (mapcar #'40ants-routes/breadcrumbs:breadcrumb-title crumbs)))) ("/" "/admin/" "/admin/users/" "/admin/users/john") ("Home" "Admin" "Users" "John Smith")
This makes it easy to create meaningful breadcrumb navigation that adapts to the content being displayed.
API Reference
40ANTS-ROUTES/BREADCRUMBS
package 40ants-routes/breadcrumbs
Classes
BREADCRUMB
class 40ants-routes/breadcrumbs:breadcrumb
()
Readers
reader 40ants-routes/breadcrumbs:breadcrumb-path
(breadcrumb) (:path)
reader 40ants-routes/breadcrumbs:breadcrumb-route
(breadcrumb) (:route)
reader 40ants-routes/breadcrumbs:breadcrumb-title
(breadcrumb) (:title)
Functions
function 40ants-routes/breadcrumbs:get-breadcrumbs
Generate breadcrumbs list for the current URL
set by 40ants-routes/with-url:with-url
macro.
function 40ants-routes/breadcrumbs:make-breadcrumb
title
Creates a breadcrumb item.
40ANTS-ROUTES/DEFROUTES
package 40ants-routes/defroutes
Functions
function 40ants-routes/defroutes:include
routes &key (path "/")
Macros
macro 40ants-routes/defroutes:defroutes
(var-name &key namespace (routes-class 'routes)) &body route-definitions
Define a variable holding collection of routes and binds it to a variable VAR-NAME
.
This macro acts like a DEFVAR
- if there is already an 40ants-routes/routes:routes
(1
2
)
object bound to the variable, then it is not replaced, but updated inplace.
This allows to change routes on the fly even if they were included into some routes
hierarchy.
You can use ROUTES-CLASS
argument to supply you own class, inherited from routes
(1
2
).
This way it might be possible to special processing for these routes, for example,
inject some special code for representing this routes in the "breadcrumbs".
Use get
, post
, put
, DELETE
macros in ROUTE-DEFINITIONS
forms.
See more examples how to define routes in the
Defining Routes
section.
macro 40ants-routes/defroutes:get
(path &key name title (route-class 'route)) &body handler-body
macro 40ants-routes/defroutes:post
(path &key name title (route-class 'route)) &body handler-body
macro 40ants-routes/defroutes:put
(path &key name title (route-class 'route)) &body handler-body
40ANTS-ROUTES/ERRORS
package 40ants-routes/errors
Classes
ARGUMENT-MISSING-ERROR
condition 40ants-routes/errors:argument-missing-error
(error)
Readers
reader 40ants-routes/errors:argument-missing-error-parameter
(argument-missing-error) (:missing-parameter)
reader 40ants-routes/errors:argument-missing-error-route-name
(argument-missing-error) (:route-name)
NAMESPACE-DUPLICATION-ERROR
condition 40ants-routes/errors:namespace-duplication-error
(error)
Readers
reader 40ants-routes/errors:existing-namespace
(namespace-duplication-error) (:namespace)
reader 40ants-routes/errors:existing-route
(namespace-duplication-error) (:existing-route)
reader 40ants-routes/errors:new-route
(namespace-duplication-error) (:new-route)
NO-COMMON-ELEMENTS-ERROR
condition 40ants-routes/errors:no-common-elements-error
(error)
Readers
reader 40ants-routes/errors:full-namespace
(no-common-elements-error) (:full-namespace)
reader 40ants-routes/errors:relative-namespace
(no-common-elements-error) (:relative-namespace)
NO-ROUTE-FOR-URL-ERROR
condition 40ants-routes/errors:no-route-for-url-error
(error)
Readers
reader 40ants-routes/errors:error-routes-path
(no-route-for-url-error) (:routes-path)
reader 40ants-routes/errors:error-url
(no-route-for-url-error) (:url)
PATH-DUPLICATION-ERROR
condition 40ants-routes/errors:path-duplication-error
(error)
Readers
reader 40ants-routes/errors:existing-path
(path-duplication-error) (:path)
reader 40ants-routes/errors:existing-route
(path-duplication-error) (:existing-route)
reader 40ants-routes/errors:new-route
(path-duplication-error) (:new-route)
URL-RESOLUTION-ERROR
condition 40ants-routes/errors:url-resolution-error
(error)
Readers
reader 40ants-routes/errors:namespace
(url-resolution-error) (:namespace)
reader 40ants-routes/errors:route-name
(url-resolution-error) (:route-name)
40ANTS-ROUTES/FIND-ROUTE
package 40ants-routes/find-route
Functions
function 40ants-routes/find-route:find-route
name &key namespace on-match
Find a route by name in the given namespace hierarchy.
If route was found, then returns it.
Additionally, it will call ON-MATCH
callable argument
with each route node along path to the leaf route.
40ANTS-ROUTES/GENERICS
package 40ants-routes/generics
Generics
generic-function 40ants-routes/generics:add-route
routes route-or-routes-to-add &key override
Add a route or included-routes object to the routes collection at runtime. If a route with the same path or namespace already exists, an error will be signaled unless override is set to true.
generic-function 40ants-routes/generics:format-url
obj stream args
Should write a piece of URL
to the STREAM
substituting arguments from plist ARGS
.
When called, it should write a piece of URL
without starting backslash.
generic-function 40ants-routes/generics:get-route-breadcrumbs
node
Returns a list of breadcrumbs associated with given routes node.
NODE
argument could have 40ants-routes/route:route
class, 40ants-routes/routes:routes
class or an object of other
class bound to some object of 40ants-routes/route:route
class.
For objects of class 40ants-routes/routes:routes
usually the method return breadcrumbs of the
route having the /
path.
Method can return from zero to N objects of 40ants-routes/breadcrumbs:breadcrumb
class.
A returning of multiple breadcrumbs can be useful if route matches to some filename in a nested directory
and you want to give an ability to navigate into intermediate directories.
generic-function 40ants-routes/generics:has-namespace-p
routes
Returns T of node can respond to node-namespace
generic-function call.
generic-function 40ants-routes/generics:match-url
obj url &key on-match
Checks for complete match of the object to URL
.
Should return an OBJ
if it fully matches to a given url.
May return a sub-object if OBJ
matches to a prefix
and sub-object matches the rest of URL
.
If match was found, the second returned value should be a alist with matched parameters.
If ON-MATCH
argument is given, then in any case
of match, full or prefix, calls ON-MATCH
function with OBJ
as a single argument.
generic-function 40ants-routes/generics:node-namespace
routes
Returns a string name of node's namepace. Works only for objects for which has-namespace-p
returns true.
generic-function 40ants-routes/generics:partial-match-url
obj url
Tests of obj matches to the a prefix of URL
.
If match was found, should return two values: the object which matches and position of the character after the matched prefix.
If OBJ
is a compound element, then
a sub-element can be returned in case of match.
generic-function 40ants-routes/generics:url-path
obj
Returns the 40ants-routes/url-pattern:url-pattern
associated with the object.
40ANTS-ROUTES/HANDLER
package 40ants-routes/handler
Functions
function 40ants-routes/handler:call-handler
Calls a handler of current route.
Should be called only during 40ants-routes/with-url:with-url
macro body execution.
40ANTS-ROUTES/INCLUDED-ROUTES
package 40ants-routes/included-routes
Classes
INCLUDED-ROUTES
class 40ants-routes/included-routes:included-routes
()
Readers
reader 40ants-routes/included-routes:original-routes
(included-routes) (:original-collection)
The original collection that was included
reader 40ants-routes/generics:url-path
(included-routes) (:path)
Path to add to all routes in the collection
Functions
function 40ants-routes/included-routes:included-routes-p
obj
40ANTS-ROUTES/MATCHED-ROUTE
package 40ants-routes/matched-route
Classes
MATCHED-ROUTE
class 40ants-routes/matched-route:matched-route
()
Readers
reader 40ants-routes/matched-route:matched-route-parameters
(matched-route) (:parameters = nil)
Parameters extracted from the URL
pattern as alist where keys are parameter names and values - parameter types.
reader 40ants-routes/matched-route:original-route
(matched-route) (:original-route)
The original route
object which has been matched.
Functions
function 40ants-routes/matched-route:matched-route-p
obj
40ANTS-ROUTES/ROUTE
package 40ants-routes/route
Classes
ROUTE
class 40ants-routes/route:route
()
Readers
reader 40ants-routes/route:route-handler
(route) (:handler)
Function to handle the route
reader 40ants-routes/route:route-method
(route) (:method = :get)
HTTP
method (GET
, POST
, PUT
, etc.)
reader 40ants-routes/route:route-name
(route) (:name)
Name of the route
reader 40ants-routes/route:route-title
(route) (:title = nil)
Title for breadcrumbs
reader 40ants-routes/generics:url-path
(route) (:pattern)
URL
pattern
URL-PATTERN
class 40ants-routes/url-pattern:url-pattern
()
Readers
reader 40ants-routes/url-pattern:url-pattern-params
(url-pattern) (:params)
Alist with parameter types
reader 40ants-routes/url-pattern:url-pattern-pattern
(url-pattern) (:pattern)
reader 40ants-routes/url-pattern:url-pattern-regex
(url-pattern) (:regex)
Functions
function 40ants-routes/route:current-route
Returns the current route.
Should be called only during 40ants-routes/with-url:with-url
macro body execution.
function 40ants-routes/route:current-route-p
Returns T if there current route matching the URL
was found..
Should be called only during 40ants-routes/with-url:with-url
or 40ants-routes/with-url:with-partially-matched-url
macro body execution.
function 40ants-routes/route:routep
obj
Checks if OBJ
is of route
class.
40ANTS-ROUTES/ROUTE-URL
package 40ants-routes/route-url
Functions
function 40ants-routes/route-url:route-url
name &rest args &key namespace &allow-other-keys
Generate a URL
for a named route with the given parameters.
40ANTS-ROUTES/ROUTES
package 40ants-routes/routes
Classes
ROUTES
class 40ants-routes/routes:routes
()
Readers
reader 40ants-routes/routes:children-routes
(routes) (:children = nil)
List of children in this collection.
reader 40ants-routes/generics:node-namespace
(routes) (:namespace)
Namespace of this routes collection.
Accessors
accessor 40ants-routes/routes:children-routes
(routes) (:children = nil)
List of children in this collection.
accessor 40ants-routes/generics:node-namespace
(routes) (:namespace)
Namespace of this routes collection.
Functions
function 40ants-routes/routes:routesp
obj
Checks if object is of class routes
.
Macros
macro 40ants-routes/routes:routes
(namespace &key (routes-class 'routes)) &body route-definitions
Define a variable holding collection of routes the same way
as 40ants-routes/defroutes:defroutes
does, but do not bind these routes to the variable.
40ANTS-ROUTES/URL-PATTERN
package 40ants-routes/url-pattern
Classes
URL-PATTERN
class 40ants-routes/url-pattern:url-pattern
()
Readers
reader 40ants-routes/url-pattern:url-pattern-params
(url-pattern) (:params)
Alist with parameter types
reader 40ants-routes/url-pattern:url-pattern-pattern
(url-pattern) (:pattern)
reader 40ants-routes/url-pattern:url-pattern-regex
(url-pattern) (:regex)
Functions
function 40ants-routes/url-pattern:parse-url-pattern
pattern
Parse a URL
pattern and extract parameter specifications.
Returns an object of class url-pattern
.
function 40ants-routes/url-pattern:url-pattern-equal
left right
Compares two url-pattern
objects
function 40ants-routes/url-pattern:url-pattern-p
obj
40ANTS-ROUTES/WITH-URL
package 40ants-routes/with-url
Macros
macro 40ants-routes/with-url:with-partially-matched-url
(root-routes url) &body body
Execute body with the current routes object corresponding to a given URL
argument.
Difference between this macro and with-url
macro is that with-url
signals an error
if it is unable to find a leaf route matching to the whole URL
.
with-partially-matched-url
will try to find a routes path matching as much
of URL
as possible. As the result, 40ants-routes/route:current-route-p
function
might return NIL
when URL
was not fully matched by with-partially-matched-url
.
macro 40ants-routes/with-url:with-url
(root-routes url) &body body
Execute body with the current routes object corresponding to a given URL
argument.