Ningle Tutorial 8: Mounting Middleware
by NMunro
Contents
- Part 1 (Hello World)
- Part 2 (Basic Templates)
- Part 3 (Introduction to middleware and Static File management)
- Part 4 (Forms)
- Part 5 (Environmental Variables)
- Part 6 (Database Connections)
- Part 7 (Envy Configuation Switching)
- Part 8 (Mouning Middleware)
Introduction
Welcome back to this Ningle tutorial series, in this part we are gonna have another look at some middleware, now that we have settings and configuration done there’s another piece of middleware we might want to look at; application mounting
, many web frameworks have the means to use apps within other apps, you might want to do this because you have some functionality you use over and over again in many projects, it makes sense to make it into an app and simply include it in other apps. You might also might want to make applications available for others to use in their applications.
Which is exactly what we are gonna do here, we spent some time building a registration view, but for users we might want to have a full registration system that will have:
- Register
- Login
- Logout
- Account Verification
- Account Reset
- Account Deletion
Creating the auth app
We will begin by building the basic views that return a simple template and mount them into our main application, we will then fill the actual logic out in another tutorial. So, we will create a new Ningle project that has 6 views that simply handle get
requests, the important thing to bear in mind is that we will have to adjust the layout of our templates, we need our auth app to use its own templates, or use the templates of a parent app, this means we will have to namespace our templates, if you have use django before this will seem familiar.
Using my project builder set up a new project for our authentication application.
(nmunro:make-project #p"~/quicklisp/local-projects/ningle-auth/")
This will create a project skeleton, complete with an asd
file, a src
, and tests
directory. In the asd
file we need to add some packages (we will add more in a later tutorial).
:depends-on (:cl-dotenv
:clack
:djula
:envy-ningle
:mito
:ningle)
In the src/main.lisp
file, we will add the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
(defpackage ningle-auth
(:use :cl)
(:export #:*app*
#:start
#:stop))
(in-package ningle-auth)
(defvar *app* (make-instance 'ningle:app))
(djula:add-template-directory (asdf:system-relative-pathname :ningle-auth "src/templates/"))
(setf (ningle:route *app* "/register")
(lambda (params)
(format t "Test: ~A~%" (mito:retrieve-by-sql "SELECT 2 + 3 AS result"))
(djula:render-template* "auth/register.html" nil :title "Register")))
(setf (ningle:route *app* "/login")
(lambda (params)
(djula:render-template* "auth/login.html" nil :title "Login")))
(setf (ningle:route *app* "/logout")
(lambda (params)
(djula:render-template* "auth/logout.html" nil :title "Logout")))
(setf (ningle:route *app* "/reset")
(lambda (params)
(djula:render-template* "auth/reset.html" nil :title "Reset")))
(setf (ningle:route *app* "/verify")
(lambda (params)
(djula:render-template* "auth/verify.html" nil :title "Verify")))
(setf (ningle:route *app* "/delete")
(lambda (params)
(djula:render-template* "auth/delete.html" nil :title "Delete")))
(defmethod ningle:not-found ((app ningle:<app>))
(declare (ignore app))
(setf (lack.response:response-status ningle:*response*) 404)
(djula:render-template* "error.html" nil :title "Error" :error "Not Found"))
(defun start (&key (server :woo) (address "127.0.0.1") (port 8000))
(djula:add-template-directory (asdf:system-relative-pathname :ningle-auth "src/templates/"))
(djula:set-static-url "/public/")
(clack:clackup
(lack.builder:builder (envy-ningle:build-middleware :ningle-auth/config *app*))
:server server
:address address
:port port))
(defun stop (instance)
(clack:stop instance))
Just as we did with our main application, we will need to create a src/config.lisp
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(defpackage ningle-auth/config
(:use :cl :envy))
(in-package ningle-auth/config)
(dotenv:load-env (asdf:system-relative-pathname :ningle-auth ".env"))
(setf (config-env-var) "APP_ENV")
(defconfig :common
`(:application-root ,(asdf:component-pathname (asdf:find-system :ningle-auth))))
(defconfig |test|
`(:debug T
:middleware ((:session)
(:mito (:sqlite3 :database-name ,(uiop:getenv "SQLITE_DB_NAME"))))))
Now, I mentioned that the template files need to be organised in a certain way, we will start with the new template layout in our auth application, the directory structure should look like this:
➜ ningle-auth git:(main) tree .
.
├── ningle-auth.asd
├── README.md
├── src
│ ├── config.lisp
│ ├── main.lisp
│ └── templates
│ ├── ningle-auth
│ │ ├── delete.html
│ │ ├── login.html
│ │ ├── logout.html
│ │ ├── register.html
│ │ ├── reset.html
│ │ └── verify.html
│ ├── base.html
│ └── error.html
└── tests
└── main.lisp
So in your src/templates
directory there will be a directory called ningle-auth
and two files base.html
and error.html
, it is important that this structure is followed, as when the app is used as part of a larger app, we want to be able to layer templates, and this is how we do it.
base.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!doctype html>
<html lang="en">
<head>
<title>{{ title }}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
{% block content %}
{% endblock %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
error.html
1
2
3
4
5
6
7
8
9
10
11
{% extends "base.html" %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-12">
<h1>{{ error }}</h1>
</div>
</div>
</div>
{% endblock %}
Now the rest of the html files are similar, with only the title changing. Using the following html, create files for:
delete.html
1
2
3
4
5
6
7
8
9
10
11
{% extends "base.html" %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-12">
<h1>Delete</h1>
</div>
</div>
</div>
{% endblock %}
login.html
1
2
3
4
5
6
7
8
9
10
11
{% extends "base.html" %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-12">
<h1>Login</h1>
</div>
</div>
</div>
{% endblock %}
logout.html
1
2
3
4
5
6
7
8
9
10
11
{% extends "base.html" %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-12">
<h1>Logout</h1>
</div>
</div>
</div>
{% endblock %}
register.html
1
2
3
4
5
6
7
8
9
10
11
{% extends "base.html" %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-12">
<h1>Register</h1>
</div>
</div>
</div>
{% endblock %}
reset.html
1
2
3
4
5
6
7
8
9
10
11
{% extends "base.html" %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-12">
<h1>Reset</h1>
</div>
</div>
</div>
{% endblock %}
verify.html
1
2
3
4
5
6
7
8
9
10
11
{% extends "base.html" %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-12">
<h1>Verify</h1>
</div>
</div>
</div>
{% endblock %}
There is one final file to create, the .env
file! Even though this application wont typically run on its own, we will use one to test it is all working, since we did write src/config.lisp
afterall!
1
2
APP_ENV=test
SQLITE_DB_NAME=ningle-auth.db
Testing the auth app
Now that the auth application has been created we will test that it at least runs on its own, once we have confirmed this, we can integrate it into our main app. Like with our main application, we will load the system and run the start function that we defined.
(ql:quickload :ningle-auth)
To load "ningle-auth":
Load 1 ASDF system:
ningle-auth
; Loading "ningle-auth"
..................................................
[package ningle-auth/config].
(:NINGLE-AUTH)
(ningle-auth:start)
NOTICE: Running in debug mode. Debugger will be invoked on errors.
Specify ':debug nil' to turn it off on remote environments.
Woo server is started.
Listening on 127.0.0.1:8000.
#S(CLACK.HANDLER::HANDLER
:SERVER :WOO
:SWANK-PORT NIL
:ACCEPTOR #<BT2:THREAD "clack-handler-woo" {1203E4E3E3}>)
*
If this works correctly, you should be able to access the defined routes in your web browser, if not, and there is an error, check that another web server isn’t running on port 8000 first! When you are able to access the simple routes from your web browser, we are ready to integrate this into our main application!
Integrating the auth app
Made it this far? Congratulations, we are almost at the end, I’m sure you’ll be glad to know, there isn’t all that much more to do, but we do have to ensure we follow the structure we set up in the auth app, which we will get to in just a moment, first, lets remember to add the ningle-auth
app to our dependencies in our project asd
file.
:depends-on (:cl-dotenv
:clack
:djula
:cl-forms
:cl-forms.djula
:cl-forms.ningle
:envy
:envy-ningle
:ingle
:mito
:mito-auth
:ningle
:ningle-auth) ;; add this
Next, we need to move most of our template files into a directory called main
, to make things easy, the only two templates we will not move are base.html
and error.html
; create a new directory src/templates/main
and put everything else in there.
For reference this is what your directory structure should look like:
➜ ningle-tutorial-project git:(main) tree .
.
├── ningle-tutorial-project.asd
├── ntp.db
├── README.md
├── src
│ ├── config.lisp
│ ├── forms.lisp
│ ├── main.lisp
│ ├── migrations.lisp
│ ├── models.lisp
│ ├── static
│ │ ├── css
│ │ │ └── main.css
│ │ └── images
│ │ ├── logo.jpg
│ │ └── lua.jpg
│ └── templates
│ ├── base.html
│ ├── error.html
│ └── main
│ ├── index.html
│ ├── login.html
│ ├── logout.html
│ ├── people.html
│ ├── person.html
│ └── register.html
└── tests
└── main.lisp
With the templates having been moved, we must find all areas in src/main.lisp
where we reference one of these templates and point to the new location, thankfully there’s only 4 lines that need to be changed, the render-template* calls, below is what they should be changed to.
(djula:render-template* "main/index.html" nil :title "Home" :user user :posts posts)
(djula:render-template* "main/people.html" nil :title "People" :users users)
(djula:render-template* "main/person.html" nil :title "Person" :user user)
(djula:render-template* "main/register.html" nil :title "Register" :form form)
Here is a complete listing of the file in question.
(defpackage ningle-tutorial-project
(:use :cl :sxql)
(:import-from
:ningle-tutorial-project/forms
#:email
#:username
#:password
#:password-verify
#:register)
(:export #:start
#:stop))
(in-package ningle-tutorial-project)
(defvar *app* (make-instance 'ningle:app))
(setf (ningle:route *app* "/")
(lambda (params)
(let ((user (list :username "NMunro"))
(posts (list (list :author (list :username "Bob") :content "Experimenting with Dylan" :created-at "2025-01-24 @ 13:34")
(list :author (list :username "Jane") :content "Wrote in my diary today" :created-at "2025-01-24 @ 13:23"))))
(djula:render-template* "main/index.html" nil :title "Home" :user user :posts posts))))
(setf (ningle:route *app* "/people")
(lambda (params)
(let ((users (mito:retrieve-dao 'ningle-tutorial-project/models:user)))
(djula:render-template* "main/people.html" nil :title "People" :users users))))
(setf (ningle:route *app* "/people/:person")
(lambda (params)
(let* ((person (ingle:get-param :person params))
(user (first (mito:select-dao
'ningle-tutorial-project/models:user
(where (:or (:= :username person)
(:= :email person)))))))
(djula:render-template* "main/person.html" nil :title "Person" :user user))))
(setf (ningle:route *app* "/register" :method '(:GET :POST))
(lambda (params)
(let ((form (cl-forms:find-form 'register)))
(if (string= "GET" (lack.request:request-method ningle:*request*))
(djula:render-template* "main/register.html" nil :title "Register" :form form)
(handler-case
(progn
(cl-forms:handle-request form) ; Can throw an error if CSRF fails
(multiple-value-bind (valid errors)
(cl-forms:validate-form form)
(when errors
(format t "Errors: ~A~%" errors))
(when valid
(cl-forms:with-form-field-values (email username password password-verify) form
(when (mito:select-dao 'ningle-tutorial-project/models:user
(where (:or (:= :username username)
(:= :email email))))
(error "Either username or email is already registered"))
(when (string/= password password-verify)
(error "Passwords do not match"))
(mito:create-dao 'ningle-tutorial-project/models:user
:email email
:username username
:password password)
(ingle:redirect "/people")))))
(error (err)
(djula:render-template* "error.html" nil :title "Error" :error err))
(simple-error (csrf-error)
(setf (lack.response:response-status ningle:*response*) 403)
(djula:render-template* "error.html" nil :title "Error" :error csrf-error)))))))
(defmethod ningle:not-found ((app ningle:<app>))
(declare (ignore app))
(setf (lack.response:response-status ningle:*response*) 404)
(djula:render-template* "error.html" nil :title "Error" :error "Not Found"))
(defun start (&key (server :woo) (address "127.0.0.1") (port 8000))
(djula:add-template-directory (asdf:system-relative-pathname :ningle-tutorial-project "src/templates/"))
(djula:set-static-url "/public/")
(clack:clackup
(lack.builder:builder (envy-ningle:build-middleware :ningle-tutorial-project/config *app*))
:server server
:address address
:port port))
(defun stop (instance)
(clack:stop instance))
The final step we must complete is actually mounting our ningle-auth
application into our main app, which is thankfully quite easy. Mounting middleware exists for ningle
and so we can configure this in src/config.lisp
, to demonstrate this we will add it to our sqlite
config:
1
2
3
4
5
6
(defconfig |sqlite|
`(:debug T
:middleware ((:session)
(:mito (:sqlite3 :database-name ,(uiop:getenv "SQLITE_DB_NAME")))
(:mount "/auth" ,ningle-auth:*app*) ;; This line!
(:static :root ,(asdf:system-relative-pathname :ningle-tutorial-project "src/static/") :path "/public/"))))
You can see on line #5 that a new mount
point is being defined, we are mounting all the routes that ningle-auth
has, onto the /auth
prefix. This means that, for example, the /register
route in ningle-auth
will actually be accessed /auth/register
.
If you can check that you can access all the urls to confirm this works, then we have assurances that we are set up correctly, however we need to come back to the templates one last time.
The reason we changed the directory structure, because ningle-auth is now running in the context of our main app, we can actually override the templates, so if we wanted to, in our src/templates
directory, we could create a ningle-auth
directory and create our own register.html
, login.html
, etc, allowing us to style and develop our pages as we see fit, allowing complete control to override, if that is our wish. By NOT moving the base.html
and error.html
files, we ensure that templates from another app can inherit our styles and layouts in a simple and predictable manner.
Conclusion
Wow, what a ride… Thanks for sticking with it this month, although, next month isn’t going to be much easier as we begin to develop a real authentication application for use in our microblog app! As always, I hope you have found this helpful and you have learned something.
In this tutorial you should be able to:
- Explain what mounting an application means
- Describe how routes play a part in mounting an application
- Justify why you might mount an application into another
- Develop and mount an application inside another
Github
- The link for this tutorials code is available here.
- The link for the auth app code is available here.