Tutorial
Let's build your first application with Clojure Stack Lite
Now that we've created a new project, we're ready to get started. Let's build a simple application together.
After Generating Your Project
The first recommended step is to check the code formatting, linting, tests, and outdated dependencies:
Next, let's initialize a git repository and commit the initial setup:
Starting the Server
Navigate to your project root directory and start the REPL:
Once in the REPL, run the application system which includes the server, database, and TailwindCSS CLI components:
Tip
Modifying the Starter Page
Let's make a simple change to the starter page to see how updates work. Open the src/myproject/views.clj
file
and replace the :h1
UI component at function home-page
with [:h1 {:class ["text-5xl"]} "Hello world!"]
.
After reloading the page in your browser, you'll see that the title has been updated.
Application System Components
The entry point of the application in production environment is src/myproject/core.clj
. It contains a main
function that runs the system with the production profile.
We use Integrant as a framework for managing all components of the system. The main config file where the entire system is described lives at resources/config.edn
.
The core components are db
and server
, which are defined in src/myproject/db.clj
and src/myproject/server.clj
respectively.
The default configuration of the server component is provided by the
reitit-extras
small helper library for convenience. It offers several options to extend the server configuration.
If you need to customize or replace anything in the server configuration, you can copy the server definition from reitit-extras
to your project.
In development mode, the entry point is dev/user.clj
. Here we start the same system with the dev profile and an extension from resources/config.dev.edn
that contains
an additional component to run the tailwindcss
CLI tool in watch mode. This automatically reloads CSS styles when files change. Since it's started as part of the application system, we don't need to
run it separately.
Implementing Our Application
Now that everything is set up, we're ready to create our application. We'll build a movie list application called "Movies Lite". This application will allow users to add new movies, view the list of movies, and delete entries from the list.
Here's a mockup of the page we're going to build:
Database Structure
For simplicity, we'll create a single table called movie
with the fields id
, title
, year
, and director
.
To create this schema, add the following database migration by creating a new file resources/migrations/0002.up.sql
with this content:
Migrations are applied automatically when the system starts. To apply our new migration, run (reset)
in the REPL.
The database file will be created in the db
directory of the project with the name myproject.sqlite
.
Afterward, you can connect to the database using any SQLite client (such as TablePlus) to verify that the movie
table was created successfully:
Application UI Layout
To visualize all parts of the application, let's create a UI layout for our main page.
Let's examine src/myproject/views.clj
. This namespace contains functions that convert data to Hiccup templates,
which will be transformed into HTML during the response rendering process. The namespace includes a base
function that provides the default setup for all pages,
including the page title, static files, and a placeholder for any content we want to display.
According to our application mockup, the page features a title and a table listing movies. Each row has a "Delete" button in the last column. At the bottom of the table, there's a form for adding new movies.
Our home-page
function and additional functions will look like:
We've separated the list item, form, and form inputs into individual components for better organization and reusability. This approach will be particularly helpful when we need to render these components separately for newly created movies in the next section.
Now, let's update our home-handler
to pass some sample data for movies
to display on the page:
After running (reset)
in the REPL and refreshing page in the browser it will look like:
Creating Movies
Now that we have the basic UI components for the table and form, let's implement the functionality to create new movies. This will involve adding database queries, creating a handler, and defining a route for our movie creation feature.
Backend Implementation
First, let's create the database queries needed for retrieving and creating movies. Create a new file src/myproject/queries.clj
with the following content:
These two functions handle our database operations:
get-movie-list
retrieves all movies from the database, ordered by IDcreate-movie
inserts a new movie record and returns the created entry with all its fields
Next, we'll create a handler for processing movie creation requests:
Let's break down how this handler works:
- The handler receives a
request
map as its single parameter - From this request, it extracts:
- The
router
from:reitit.core/router
- The application
context
and formparams
- The
- It then:
- Creates a new movie in the database using
queries/create-movie
- Renders a fresh form and the newly created movie as a table row
- Returns the HTML response with the appropriate content type
- Creates a new movie in the database using
The :context
key is automatically added to the request by the wrap-context
middleware. This context contains all system components, including our database connection pool.
The response uses HTMX's out-of-band swap feature to add the new movie row to the end of our table without refreshing the entire page. The hx-swap-oob
attribute targets the table body with the ID table-content
that we defined earlier.
Now, let's define a route for this handler in our routes configuration:
We're intentionally not adding a schema for request parameters in the route definition. Instead, we'll validate parameters directly in the handler (in a later section). This approach gives us more flexibility to return validation errors as part of the HTML response.
We retrieve parameters from the params
key of the request, which is provided by the wrap-nested-params
middleware, rather than using the parameters-middleware
.
Finally, we need to update our home page handler to fetch movies from the database instead of using static data:
Frontend Implementation
With our backend ready, let's update the frontend to connect our form to the new route. We'll modify the form to send data to our backend and handle the response appropriately.
Let's examine the key changes we've made to connect our frontend with the backend:
-
Required imports:
- We import
reitit-extras.core
for route generation and CSRF protection - We import
myproject.routes
as an alias to reference our route names
- We import
-
Form component enhancements:
- We've updated the
form-input
function to accept and display field values - The
form
component now acceptsrouter
andparams
as arguments
- We've updated the
-
HTMX integration:
:hx-post
- Specifies the endpoint for form submission using the router to generate the correct URL:hx-target
- Targets the form itself for replacement:hx-swap
- Replaces the entire form with the response content(reitit-extras/csrf-token-html)
- A CSRF token for secure form submissions
-
Home page updates:
- The
home-page
function now accepts bothmovies
androuter
parameters - It passes the router to the form component to enable proper URL generation
- The
When a user submits the form, HTMX will send a POST request to our backend. The handler will process the request, create a new movie, and return HTML that includes:
- A fresh form (replacing the current one)
- A new table row with the movie data (added to the table via the out-of-band swap)
Now, after reloading the system in the REPL with (reset)
, we should be able to create new movies using our form:
Form Validation
Our form can create a movie, but what happens if a user doesn't fill in required fields? Without validation, we'd store empty values to the database. Let's implement proper validation to provide a better user experience.
We'll add parameter validation before inserting data into the database and display user-friendly error messages on the page.
Typically, we would define parameter validation as part of the router using Malli schemas. However, to return custom HTML with validation messages directly in the form, we'll implement validation in the handler instead.
Now let's update our frontend components to display validation errors. We'll modify both the form-input
and form
components to handle and display error messages.
After resetting the system in the REPL with (reset)
and refreshing the page, we can test our validation by submitting the form with empty fields. The system will display appropriate error messages for each invalid field:
How the Validation Flow Works
- User submits the form -> HTMX sends a POST request to our endpoint
- Server validates the input:
- If invalid -> Returns the form with error messages
- If valid -> Creates the movie and returns updated HTML
- HTMX updates the DOM:
- Replaces the form with the response
- Adds the new movie row to the table (if validation passed)
This approach provides immediate feedback to users without page refreshes, creating a smooth experience while maintaining the simplicity of Server-side Rendering.
Delete a Movie
To delete a movie, let's add a query, handler, and route for it.
The route we will add as a su-route for existing /movies
route, and it will look like this:
Now we can add a delete button to each row of the table. We'll use the :hx-delete
attribute to send a request to the server. We'll add "closest tr"
as :hx-target
to remove the row from the table. We also need to add a CSRF token to the request header.
After refreshing the system in REPL with (reset)
, we can remove the row from the table by clicking on the delete button.
Testing
We have a simple application that we can test. Let's start with fixing the existing test:
Now if you run bb test
, it should pass:
Testing list of movies
We can update this test to check if we can show a movie on a page:
If you run bb test
, it should pass.
Testing movie creation
Now we can add a test for creating a movie:
Notice how we get the CSRF token from the page and add it to the request header using cookies.
And one for deleting a movie:
All tests should pass and this time the coverage should higher:
Check linting and formatting
After some changes, we can check if everything is still ok with our code:
If there are some formatting changes we can commit them.
Summary
In this quick tutorial, we created a simple application with Clojure Stack Lite.
We've seen how to create a new project, start the server, and build a simple application with a form and a table.
We performed POST
, GET
, and DELETE
requests using HTMX, with form validation and error handling.
For real-world applications, you might want to add more features like user registration and authentication, list pagination, etc. This tutorial provides a basic foundation to build your own application.
Next steps
From here, you can proceed with deploying your application to production or learn more about project structure and management in the "Guide" section.