This guide walks you through creating a simple TODO management application using clails. You'll learn how to create a database, implement models, views, and controllers, and start a web application step by step.
- Roswell must be installed
- clails must be installed
For installation instructions, please refer to README.md.
- Docker must be installed
- Docker Compose must be installed
First, create a new project using the clails new command.
clails new todoapp
cd todoappThis creates the basic structure for a clails application.
The following files are generated during project creation:
Makefile- A collection of commands to simplify development with Dockerdocker/Dockerfile.dev- Docker image definition for developmentdocker/docker-compose.dev.yml- Docker Compose configurationdocker/dev.env- Environment variable settings for development
make buildTo specify a clails branch or tag, use the CLAILS_BRANCH variable (defaults to develop if not specified).
# branch
CLAILS_BRANCH=release/0.0.2 make build
# tag
CLAILS_BRANCH=v0.0.1 make buildOnce the image build is complete, the development environment is ready.
make upThe container starts in the background.
In Docker environment, use make commands to perform database operations.
make db.createBy default, a SQLite3 database is created.
To use MySQL or PostgreSQL, specify the --database option when creating the project.
Available Make Commands:
make build- Build Docker imagemake rebuild- Rebuild Docker image without cachemake up- Start containersmake down- Stop containersmake console- Start shell inside containermake logs- Show application logsmake db.create- Create databasemake db.migrate- Run migrationsmake db.rollback- Rollback migrationsmake db.seed- Seed database
Create the database using the clails db:create command.
clails db:createBy default, a SQLite3 database is created.
To use MySQL or PostgreSQL, specify the --database option when creating the project.
Generate Model, View, and Controller files at once using the clails generate:scaffold command.
Start a shell inside the container and run the command:
make console
# Inside the container
clails generate:scaffold todo
exitclails generate:scaffold todoThis command generates the following files:
app/models/todo.lisp- Model fileapp/views/todo/list.html- View fileapp/controllers/todo-controller.lisp- Controller filedb/migrate/YYYYMMDD-HHMMSS-todo.lisp- Migration filetest/models/todo.lisp- Model test filetest/controllers/todo-controller.lisp- Controller test file
Edit the generated migration file to define the TODO table structure.
Open db/migrate/YYYYMMDD-HHMMSS-todo.lisp and modify it as follows:
(in-package #:todoapp-db)
(defmigration "todo"
(:up #'(lambda (connection)
(create-table connection :table "todo"
:columns '(("title" :type :string
:not-null T)
("done" :type :boolean
:default NIL)
("done-at" :type :datetime))))
:down #'(lambda (connection)
(drop-table connection :table "todo"))))This table definition includes:
title- TODO title (required)done- Completion flag (default: false)done_at- Completion timestamp
After modifying the migration file, create the table using the clails db:migrate command.
make db.migrateclails db:migrateThis creates the TODO table in the database.
Open app/models/todo.lisp and add the necessary functionality for the TODO application.
(in-package #:cl-user)
(defpackage #:todoapp/models/todo
(:use #:cl
#:clails/model)
(:import-from #:clails/datetime
#:from-universal-time
#:format-datetime)
(:import-from #:local-time
#:now)
(:export #:<todo>
#:find-all
#:create-todo
#:find-by-id
#:mark-as-done
#:format-done-at))
(in-package #:todoapp/models/todo)
(defmodel <todo> (<base-model>) (:table "todo"))
(defun find-all ()
"Find all todo items.
@return [list] List of todo records
"
(let ((q (query <todo> :as :todo)))
(execute-query q nil)))
(defun create-todo (title)
"Create a new todo item with the given title.
@param title [string] Todo title
@return [<todo>] Created todo record
"
(let ((todo (make-record '<todo> :title title :done nil)))
(save todo)
todo))
(defun find-by-id (id)
"Find a todo item by ID.
@param id [integer] Todo ID
@return [<todo>] Todo record
@return [nil] NIL if not found
"
(let* ((q (query <todo> :as :todo :where (:= (:todo :id) :id-param)))
(results (execute-query q (list :id-param id))))
(car results)))
(defun mark-as-done (todo)
"Mark a todo item as done.
@param todo [<todo>] Todo record to mark as done
@return [<todo>] Updated todo record
"
(setf (ref todo :done) t)
(setf (ref todo :done-at) (now))
(save todo)
todo)
(defmethod format-done-at ((todo <todo>))
"Format the done-at timestamp of a todo item.
@param todo [<todo>] Todo record
@return [string] Formatted timestamp in MySQL format (yyyy-mm-dd hh:mm:ss)
@return [nil] NIL if done-at is not set
"
(let ((done-at (ref todo :done-at)))
(when done-at
(format-datetime (from-universal-time done-at)
:format :mysql))))This code implements the following functionality:
find-all- Retrieve all TODO itemscreate-todo- Create a new TODO itemfind-by-id- Find a TODO by IDmark-as-done- Mark a TODO as completedformat-done-at- Format the TODO's DONE-AT timestamp in MySQL format (yyyy-mm-dd hh:mm:ss) if set
Open app/views/todo/package.lisp and declare the methods used in the View.
(in-package #:cl-user)
(defpackage #:todoapp/views/todo/package
(:use #:cl)
(:import-from #:clails/view/view-helper
#:*view-context*
#:view)
(:import-from #:todoapp/models/todo
#:format-done-at))
(in-package #:todoapp/views/todo/package)Open app/views/todo/list.html and modify it to display and manipulate TODO items.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Todo List</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
h1 { color: #333; }
table { border-collapse: collapse; width: 100%; margin-top: 20px; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #4CAF50; color: white; }
tr:nth-child(even) { background-color: #f2f2f2; }
.done { text-decoration: line-through; color: #999; }
form { margin-top: 20px; }
input[type="text"] { padding: 5px; width: 300px; }
button { padding: 5px 15px; background-color: #4CAF50; color: white; border: none; cursor: pointer; }
button:hover { background-color: #45a049; }
</style>
</head>
<body>
<h1>Todo List</h1>
<h2>Add New Todo</h2>
<form action="/todo" method="POST">
<input type="text" name="title" placeholder="Enter todo title" required>
<button type="submit">Add</button>
</form>
<h2>Todo Items</h2>
<table>
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>Status</th>
<th>Done At</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<cl:loop for="todo" in="(view :todos)">
<tr>
<td><%= (clails/model:ref todo :id) %></td>
<td class="<%= (if (clails/model:ref todo :done) "done" "") %>">
<%= (clails/model:ref todo :title) %>
</td>
<td><%= (if (clails/model:ref todo :done) "Done" "Pending") %></td>
<td><%= (or (format-done-at todo) "-") %></td>
<td>
<cl:unless test="(clails/model:ref todo :done)">
<form action="/todo" method="POST" style="display:inline;">
<input type="hidden" name="_method" value="PUT">
<input type="hidden" name="id" value="<%= (clails/model:ref todo :id) %>">
<button type="submit">Mark as Done</button>
</form>
</cl:unless>
</td>
</tr>
</cl:loop>
</tbody>
</table>
<p style="margin-top: 20px; color: #666;">
Total: <%= (length (view :todos)) %> items
</p>
</body>
</html>This view implements:
- TODO addition form
- TODO list display (table format)
- Button to mark TODO as completed
- Strikethrough styling for completed TODO items
How to Implement PUT in HTML Forms:
HTML <form> tags only support GET and POST methods natively.
To send PUT or DELETE requests, use the following approach:
<form action="/todo/123" method="POST">
<input type="hidden" name="_method" value="PUT">
<button type="submit">Mark as Done</button>
</form>clails checks for the _method parameter in POST requests. When the value is "PUT", it routes to the do-put method, and when it's "DELETE", it routes to the do-delete method. This allows HTML forms to send PUT and DELETE requests.
Open app/controllers/todo-controller.lisp and modify it to handle requests.
(in-package #:cl-user)
(defpackage #:todoapp/controllers/todo-controller
(:use #:cl
#:clails/controller/base-controller)
(:import-from #:clails/controller/base-controller
#:param)
(:import-from #:todoapp/models/todo
#:find-all
#:create-todo
#:find-by-id
#:mark-as-done)
(:export #:<todo-controller>))
(in-package #:todoapp/controllers/todo-controller)
(defclass <todo-controller> (<web-controller>)
())
(defmethod do-get ((controller <todo-controller>))
"Get all todo items and display them."
(let ((todos (find-all)))
(set-view controller "todo/list.html" (list :todos todos))))
(defmethod do-post ((controller <todo-controller>))
"Create a new todo item."
(let ((title (param controller "title")))
(create-todo title)
(set-redirect controller "/todo")))
(defmethod do-put ((controller <todo-controller>))
"Update todo item to mark as done."
(let* ((id-str (param controller "id"))
(id (parse-integer id-str))
(todo (find-by-id id)))
(when todo
(mark-as-done todo))
(set-redirect controller "/todo")))This controller implements:
do-get- Display TODO listdo-post- Create a new TODOdo-put- Mark TODO as completed
Once all implementation is complete, start the server.
In Docker environment, the server automatically starts when you run make up.
make upTo check server logs:
make logsTo stop the container:
make downclails serverBy default, the server starts at http://localhost:5000.
Access http://localhost:5000/todo in your browser.
You can perform the following operations:
- Add TODO: Enter a title in the "Add New Todo" form and click the "Add" button
- View TODO list: All registered TODOs are displayed
- Complete TODO: Click the "Mark as Done" button to mark a TODO as completed
If you want to add initial data, create db/seeds.lisp.
(in-package #:cl-user)
(defpackage #:todoapp-db
(:use #:cl)
(:import-from #:clails/model
#:save
#:make-record)
(:import-from #:todoapp/models/todo
#:<todo>))
(in-package #:todoapp-db)
(defun run ()
"Create seed data for todo table."
(let ((todo1 (make-record '<todo> :title "Buy milk" :done nil))
(todo2 (make-record '<todo> :title "Read a book" :done nil))
(todo3 (make-record '<todo> :title "Write code" :done nil)))
(save todo1)
(save todo2)
(save todo3)
(format t "Created 3 todo items~%")))To load the seed data:
make db.seedclails db:seedIn this QuickStart, you learned how to create a TODO application using clails.
- Create a project (
clails new) - Build Docker image (
make build) - Start Docker container (
make up) - Create database (
make db.create) - Generate scaffold (inside container:
clails generate:scaffold) - Edit migration file
- Run migration (
make db.migrate) - Implement Model
- Implement View
- Implement Controller
- Start server (automatically started with
make up)
- Create a project (
clails new) - Create a database (
clails db:create) - Generate scaffold (
clails generate:scaffold) - Edit migration file
- Run migration (
clails db:migrate) - Implement Model
- Implement View
- Implement Controller
- Start server (
clails server)
By applying these steps, you can create more complex web applications.
- Easy setup (just need Docker)
- Entire team can share the same environment
- Database (MySQL or PostgreSQL) is automatically set up
- Doesn't pollute the host environment
Makefilemakes commonly used commands easy to execute
- Model Guide - Database operations and query details
- View Guide - Template engine details
- Controller Guide - Request handling and routing details
- Testing Guide - How to write tests