ningle-fbr

May 24, 2026 · View on GitHub

A file-based router for ningle.

Check the release notes for the latest updates.

What is File-Based Routing?

File-based routing automatically creates URL routes based on a project’s file and directory structure. Instead of manually configuring routes in a separate routing file, each file in a designated directory (e.g., pages or routes) becomes a route. This simplifies development and maintenance since adding, removing, or renaming a route is often just a matter of modifying a file’s name or location.

Usage

Practical usage example

To use ningle-fbr, set up your project using the package-inferred-system style.

Example directory structure:

example.asd
src/
  app.lisp
  routes/
    index.lisp
    hello.lisp
    users/
      index.lisp
      <id>.lisp
    docs/
      <...>.lisp
    nested/
      page.lisp

example.asd:

(defsystem "example"
  :class :package-inferred-system
  :pathname "src"
  :depends-on ("example/app"))

/src/app.lisp:

(defpackage #:example
  (:nicknames #:example/app)
  (:use #:cl)
  (:import-from #:ningle)
  (:import-from #:ningle-fbr
                #:set-routes))
(in-package #:example/app)

(defparameter *app* (make-instance 'ningle:<app>))

(set-routes *app* :system :example :dir "routes")

Static Routing

Routes are determined by packages located under :example/routes. The package’s name corresponds directly to a URL path:

  • :example/routes/index/
  • :example/routes/hello/hello
  • :example/routes/users/index/users
  • :example/routes/nested/page/nested/page

/src/routes/index.lisp:

(defpackage #:example/routes/index
  (:use #:cl)
  (:export #:@get
           #:@post
           #:@put
           #:@delete))
(in-package #:example/routes/index)

(defun @get (params)
  ;; Implement GET logic here
  )

(defun @post (params)
  ;; Implement POST logic here
  )

(defun @put (params)
  ;; Implement PUT logic here
  )

(defun @delete (params)
  ;; Implement DELETE logic here
  )

Handlers are chosen based on the HTTP method. If @get is exported, it will be called for GET requests on /.

Dynamic Routing

To define dynamic routes, use <> in the file name to indicate URL parameters.

For example: :example/routes/users/<id>/users/:id

If a request comes in at /users/123, params will include :id "123".

Catch-all (Splat) Routes

Use <...> — optionally with a descriptive suffix like <...slug> — to match the remainder of the path, including embedded slashes. The captured value is delivered under the :splat key as a list (one element per <...> segment in the route).

For example: :example/routes/docs/<...>/docs/*

A request to /docs/intro/getting-started invokes this route with params containing (:splat ("intro/getting-started")).

(defpackage #:example/routes/docs/<...>
  (:use #:cl)
  (:export #:@get))
(in-package #:example/routes/docs/<...>)

(defun @get (params)
  (let ((rest (first (cdr (assoc :splat params)))))
    (format nil "doc: ~A" rest)))

404 Handling

To handle 404 (Not Found) errors, create a special package named :example/routes/not-found and define @not-found function:

(defpackage #:example/routes/not-found
  (:use #:cl)
  (:export #:@not-found))
(in-package #:example/routes/not-found)

(defun @not-found ()
  ;; Implement custom logic here
  )

Inspecting Routes

list-routes returns a structured description of every route file ningle-fbr detects, without installing anything onto an app. Each entry is a plist with :path, :uri, :package, :kind (:static, :dynamic, :catch-all, or :not-found), and :methods (the exported HTTP method handlers).

(ningle-fbr:list-routes :system :example :dir "routes")
;; => ((:path "/index"        :uri "/"          :kind :static    :methods (:GET) ...)
;;     (:path "/hello"        :uri "/hello"     :kind :static    :methods (:GET) ...)
;;     (:path "/users/<id>"   :uri "/users/:id" :kind :dynamic   :methods (:GET) ...)
;;     (:path "/docs/<...>"   :uri "/docs/*"    :kind :catch-all :methods (:GET) ...)
;;     (:path "/not-found"    :uri "/not-found" :kind :not-found :methods (:NOT-FOUND) ...))

Useful for printing a routes table at boot, generating docs, or sanity-checking what gets registered.

Route Conflict Detection

Both set-routes and list-routes signal ningle-fbr/router:route-conflict-error when two files would match the same set of incoming requests — for example users/<id>.lisp and users/<name>.lisp, or users.lisp and users/index.lisp. The condition exposes route-conflict-error-conflicts, an alist of (URI . PATHS) for programmatic handling.

License

Licensed under the MIT License.

© 2024 Akira Tempaku