The clails Controller is an HTTP request handler inspired by Ruby on Rails' Controller. Controllers receive HTTP requests, process data using Models, and pass data to Views or return JSON responses.
- Controllers define processing for each HTTP method (GET, POST, PUT, DELETE)
- Action-based routing allows defining multiple actions (index, show, new, edit, etc.) in a single Controller
- The
resourcesfunction provides concise RESTful route definitions - There are
<web-controller>for web applications and<rest-controller>for REST APIs - Routing tables bind URL paths to Controllers
- URL parameters are automatically extracted and accessible from the Controller
clails has three types of Controller classes.
The base class for all Controllers. Provides basic HTTP request processing.
(defclass <my-controller> (<base-controller>)
())A Controller for web applications that render HTML views.
(defclass <my-web-controller> (<web-controller>)
())A Controller for REST APIs that return structured data such as JSON.
(defclass <my-api-controller> (<rest-controller>)
())(in-package #:your-app/controller)
(defclass <users-controller> (<web-controller>)
()
(:documentation "Users controller for managing user resources"))Override methods corresponding to each HTTP method.
;; GET request handling
(defmethod do-get ((controller <users-controller>))
(let ((users (get-all-users)))
(set-view controller "users/index.html"
`(:users ,users))))
;; POST request handling
(defmethod do-post ((controller <users-controller>))
(let* ((name (param controller "name"))
(email (param controller "email"))
(user (create-user name email)))
(set-redirect controller "/users")))
;; PUT request handling
(defmethod do-put ((controller <users-controller>))
(let* ((id (param controller "id"))
(name (param controller "name"))
(user (update-user id name)))
(set-view controller "users/show.html"
`(:user ,user))))
;; DELETE request handling
(defmethod do-delete ((controller <users-controller>))
(let ((id (param controller "id")))
(delete-user id)
(set-redirect controller "/users")))HTML <form> tags only support GET and POST methods natively.
To send PUT or DELETE requests from HTML forms, use the _method parameter.
PUT Request Example:
<form action="/users/123" method="POST">
<input type="hidden" name="_method" value="PUT">
<input type="text" name="name" value="John Doe">
<button type="submit">Update</button>
</form>DELETE Request Example:
<form action="/users/123" method="POST">
<input type="hidden" name="_method" value="DELETE">
<button type="submit">Delete</button>
</form>clails checks the _method parameter in POST requests and routes as follows:
- When
_methodis"PUT"→ calls thedo-putmethod - When
_methodis"DELETE"→ calls thedo-deletemethod - When
_methodis not specified → calls thedo-postmethod
This enables REST API-like operations from HTML forms.
Routing tables bind URL paths to Controllers.
Define routes in config/routes.lisp or similar.
(in-package #:your-app/config)
(setf clails/environment:*routing-tables*
'(;; Top page
(:path "/"
:controller "your-app/controller::<top-controller>")
;; User list and creation
(:path "/users"
:controller "your-app/controller::<users-controller>")
;; User detail, update, and deletion
(:path "/users/:id"
:controller "your-app/controller::<user-controller>")
;; Nested resources
(:path "/users/:user-id/posts/:post-id"
:controller "your-app/controller::<user-posts-controller>")))
;; Initialize at application startup
(clails/controller/base-controller:initialize-routing-tables):parameter-name in URL paths are automatically extracted and accessible via the param function.
;; Route definition: "/users/:user-id/posts/:post-id"
;; Access example: GET /users/123/posts/456
(defmethod do-get ((controller <user-posts-controller>))
(let ((user-id (param controller "user-id")) ; => "123"
(post-id (param controller "post-id"))) ; => "456"
;; Processing...
))By specifying :action and :method in a route, you can define multiple action methods in a single Controller.
(setf clails/environment:*routing-tables*
'((:path "/todos"
:controller "your-app/controllers/todo-controller::<todo-controller>"
:action "index"
:method :get)
(:path "/todos/:id"
:controller "your-app/controllers/todo-controller::<todo-controller>"
:action "show"
:method :get)
(:path "/todos/new"
:controller "your-app/controllers/todo-controller::<todo-controller>"
:action "new"
:method :get)))In the corresponding Controller, define action-named methods instead of do-get.
(defclass <todo-controller> (<web-controller>)
())
(defmethod index ((controller <todo-controller>))
(set-view controller "todos/index.html"))
(defmethod show ((controller <todo-controller>))
(let ((id (param controller "id")))
(set-view controller "todos/show.html" `(:id ,id))))
(defmethod new ((controller <todo-controller>))
(set-view controller "todos/new.html"))Route matching priority:
- Routes where both path and HTTP method match (highest priority)
- Routes where only the path matches and
:methodis not specified - No match → 404
Backward compatibility: Routes without :action continue to dispatch to do-get, do-post, etc. as before.
The resources function lets you define 7 standard RESTful routes in a single line.
(setf clails/environment:*routing-tables*
`(,@(resources "todos" "your-app/controllers/todo-controller::<todo-controller>")
,@(resources "blogs" "your-app/controllers/blog-controller::<blog-controller>")))(resources "todos" "controller") generates the following 7 routes:
| HTTP Method | Path | Action | Purpose |
|---|---|---|---|
| GET | /todos | index | List |
| GET | /todos/new | new | New form |
| POST | /todos | create | Create |
| GET | /todos/:id | show | Detail |
| GET | /todos/:id/edit | edit | Edit form |
| PUT | /todos/:id | update | Update |
| DELETE | /todos/:id | destroy | Delete |
/todos/new is placed before /todos/:id, so "new" will never be matched as an :id parameter.
;; Generate only index and show
(resources "todos" "controller" :only '(:index :show));; Exclude destroy and update
(resources "todos" "controller" :except '(:destroy :update))(defclass <todo-controller> (<web-controller>)
())
(defmethod index ((controller <todo-controller>))
(let ((todos (get-all-todos)))
(set-view controller "todos/index.html" `(:todos ,todos))))
(defmethod show ((controller <todo-controller>))
(let ((todo (find-todo (param controller "id"))))
(set-view controller "todos/show.html" `(:todo ,todo))))
(defmethod new ((controller <todo-controller>))
(set-view controller "todos/new.html"))
(defmethod create ((controller <todo-controller>))
(let ((title (param controller "title")))
(create-todo title)
(set-redirect controller "/todos")))
(defmethod edit ((controller <todo-controller>))
(let ((todo (find-todo (param controller "id"))))
(set-view controller "todos/edit.html" `(:todo ,todo))))
(defmethod update ((controller <todo-controller>))
(let ((id (param controller "id"))
(title (param controller "title")))
(update-todo id title)
(set-redirect controller (format nil "/todos/~A" id))))
(defmethod destroy ((controller <todo-controller>))
(destroy-todo (param controller "id"))
(set-redirect controller "/todos"))Retrieves request parameters (query parameters, POST data, URL parameters).
(defmethod do-get ((controller <search-controller>))
(let ((query (param controller "q"))
(page (param controller "page")))
;; Search processing...
))Form data from POST requests can be retrieved the same way.
(defmethod do-post ((controller <users-controller>))
(let ((name (param controller "name"))
(email (param controller "email"))
(age (parse-integer (param controller "age"))))
;; User creation processing...
))Renders a view template with data.
(defmethod do-get ((controller <users-controller>))
(let ((users (execute-query
(query <user>
:as :user
:order-by ((:user :created-at :desc)))
'())))
(set-view controller "users/index.html"
`(:users ,users))))Data is passed to views as a property list.
(defmethod do-get ((controller <user-controller>))
(let* ((id (param controller "id"))
(user (first (execute-query
(query <user>
:as :user
:where (:= (:user :id) :id))
(list :id id)))))
(set-view controller "users/show.html"
`(:user ,user
:title ,(format nil "User: ~A" (ref user :name))
:updated-at ,(ref user :updated-at)))))Redirects to another URL.
(defmethod do-post ((controller <users-controller>))
(let* ((name (param controller "name"))
(email (param controller "email"))
(user (make-record '<user> :name name :email email)))
(if (save user)
;; Redirect to user list on success
(set-redirect controller "/users")
;; Return to form with errors on failure
(set-view controller "users/new.html"
`(:errors ,(get-errors user))))))(defmethod do-post ((controller <posts-controller>))
(let ((post (create-post (param controller "title")
(param controller "body"))))
;; Redirect to post detail page
(set-redirect controller
(format nil "/posts/~A" (ref post :id)))))Returns a JSON response. Available in <rest-controller>.
(defclass <api-users-controller> (<rest-controller>)
())
(defmethod do-get ((controller <api-users-controller>))
(let ((users (get-all-users)))
(set-response controller
`((:status . "success")
(:count . ,(length users))
(:data . ,users)))))Set status codes using the code slot.
(defmethod do-post ((controller <api-users-controller>))
(let* ((name (param controller "name"))
(user (create-user name)))
(if user
(progn
;; 201 Created
(setf (slot-value controller 'code) 201)
(set-response controller
`((:status . "success")
(:data . ,user))))
(progn
;; 400 Bad Request
(setf (slot-value controller 'code) 400)
(set-response controller
`((:status . "error")
(:message . "Failed to create user")))))))200- OK (default)201- Created204- No Content400- Bad Request401- Unauthorized403- Forbidden404- Not Found500- Internal Server Error
Retrieves environment information from the request.
(defmethod do-get ((controller <my-controller>))
(let ((path (getf (env controller) :path-info))
(method (getf (env controller) :request-method))
(headers (getf (env controller) :headers)))
;; Processing...
)):path-info- Request path:request-method- HTTP method (:GET,:POST, etc.):query-string- Query string:content-type- Content-Type header:content-length- Content-Length header:headers- All HTTP headers
(defmethod do-get ((controller <my-controller>))
(let* ((headers (getf (env controller) :headers))
(auth-header (gethash "authorization" headers))
(user-agent (gethash "user-agent" headers)))
;; Processing...
))(defmethod do-get ((controller <my-controller>))
(setf (slot-value controller 'headers)
'(("X-Custom-Header" . "Custom Value")
("Cache-Control" . "no-cache")))
(set-response controller '((:status . "success"))))(defclass <users-controller> (<web-controller>)
())
;; List (GET /users)
(defmethod do-get ((controller <users-controller>))
(let ((users (execute-query
(query <user>
:as :user
:order-by ((:user :name)))
'())))
(set-view controller "users/index.html"
`(:users ,users))))
;; Create (POST /users)
(defmethod do-post ((controller <users-controller>))
(let* ((name (param controller "name"))
(email (param controller "email"))
(user (make-record '<user> :name name :email email)))
(if (save user)
(set-redirect controller "/users")
(set-view controller "users/new.html"
`(:errors ,(get-errors user)
:user ,user)))))
(defclass <user-controller> (<web-controller>)
())
;; Detail (GET /users/:id)
(defmethod do-get ((controller <user-controller>))
(let* ((id (param controller "id"))
(user (first (execute-query
(query <user>
:as :user
:where (:= (:user :id) :id))
(list :id id)))))
(if user
(set-view controller "users/show.html"
`(:user ,user))
(error '404/not-found :path (getf (env controller) :path-info)))))
;; Update (PUT /users/:id)
(defmethod do-put ((controller <user-controller>))
(let* ((id (param controller "id"))
(user (first (execute-query
(query <user>
:as :user
:where (:= (:user :id) :id))
(list :id id)))))
(when user
(setf (ref user :name) (param controller "name"))
(setf (ref user :email) (param controller "email"))
(if (save user)
(set-redirect controller (format nil "/users/~A" id))
(set-view controller "users/edit.html"
`(:errors ,(get-errors user)
:user ,user))))))
;; Delete (DELETE /users/:id)
(defmethod do-delete ((controller <user-controller>))
(let* ((id (param controller "id"))
(user (first (execute-query
(query <user>
:as :user
:where (:= (:user :id) :id))
(list :id id)))))
(when user
(destroy user)
(set-redirect controller "/users"))))(defmethod do-post ((controller <order-controller>))
(handler-case
(with-transaction
(let* ((user-id (param controller "user-id"))
(product-id (param controller "product-id"))
(quantity (parse-integer (param controller "quantity")))
(product (first (execute-query
(query <product>
:as :product
:where (:= (:product :id) :product-id))
(list :product-id product-id)))))
;; Check stock
(unless (>= (ref product :stock) quantity)
(error "Insufficient stock"))
;; Create order
(let ((order (make-record '<order>
:user-id user-id
:product-id product-id
:quantity quantity
:total-price (* (ref product :price) quantity))))
(save order)
;; Update stock
(setf (ref product :stock) (- (ref product :stock) quantity))
(save product)
;; Redirect on success
(set-redirect controller (format nil "/orders/~A" (ref order :id))))))
(error (e)
;; Handle error
(setf (slot-value controller 'code) 400)
(set-response controller
`((:status . "error")
(:message . ,(format nil "~A" e)))))))(defclass <api-users-controller> (<rest-controller>)
())
;; List (GET /api/users)
(defmethod do-get ((controller <api-users-controller>))
(let ((users (get-all-users)))
(set-response controller
`((:status . "success")
(:count . ,(length users))
(:data . ,(mapcar #'user-to-json users))))))
;; Create (POST /api/users)
(defmethod do-post ((controller <api-users-controller>))
(let* ((name (param controller "name"))
(email (param controller "email"))
(user (make-record '<user> :name name :email email)))
(if (save user)
(progn
(setf (slot-value controller 'code) 201)
(set-response controller
`((:status . "success")
(:data . ,(user-to-json user)))))
(progn
(setf (slot-value controller 'code) 400)
(set-response controller
`((:status . "error")
(:errors . ,(get-errors-json user))))))))
(defun user-to-json (user)
`((:id . ,(ref user :id))
(:name . ,(ref user :name))
(:email . ,(ref user :email))
(:created-at . ,(format-datetime (ref user :created-at)))))(defmethod do-get ((controller <users-controller>))
(let* ((page (or (parse-integer (param controller "page") :junk-allowed t) 1))
(per-page 20)
(offset (* (1- page) per-page))
(users (execute-query
(query <user>
:as :user
:order-by ((:user :created-at :desc))
:limit per-page
:offset offset)
'()))
(total-count (count-users)))
(set-view controller "users/index.html"
`(:users ,users
:page ,page
:per-page ,per-page
:total-count ,total-count
:total-pages ,(ceiling total-count per-page)))))By default, undefined HTTP methods raise a 404/not-found error.
;; If do-get is not implemented, automatic 404 error
(defclass <my-controller> (<base-controller>)
())
;; Custom 404 error
(defmethod do-get ((controller <my-controller>))
(error '404/not-found :path (getf (env controller) :path-info)))You can create a dedicated Controller to display error pages.
(defclass <error-controller> (<web-controller>)
())
(defmethod do-get ((controller <error-controller>))
(setf (slot-value controller 'code) 500)
(set-view controller "errors/500.html"
`(:message "Internal Server Error")))Controllers should only have the following responsibilities:
- Request Validation: Check parameter existence and types
- Model Invocation: Delegate business logic to Models
- Response Construction: Set up Views or response data
Extract complex business logic into Models or Service classes.
;; Bad example: Writing business logic in Controller
(defmethod do-post ((controller <order-controller>))
(let ((product (find-product (param controller "product-id"))))
;; Complex calculations and validation...
))
;; Good example: Extract into Service class
(defmethod do-post ((controller <order-controller>))
(let ((order-service (make-instance '<order-service>)))
(create-order order-service
:user-id (param controller "user-id")
:product-id (param controller "product-id")
:quantity (param controller "quantity"))))Always validate parameters.
(defmethod do-post ((controller <users-controller>))
(let ((name (param controller "name"))
(email (param controller "email")))
;; Validation
(unless (and name email)
(setf (slot-value controller 'code) 400)
(set-response controller
`((:status . "error")
(:message . "Name and email are required")))
(return-from do-post))
;; Continue processing...
))REST APIs should follow RESTful design principles.
GET /users- ListGET /users/:id- DetailPOST /users- CreatePUT /users/:id- UpdateDELETE /users/:id- Delete
clails Controllers have the following features:
- Simple Design: Just define methods for each HTTP method
- RESTful Routing: Rails-like route definitions with the
resourcesfunction (supports:only/:exceptoptions) - Flexible Routing: Automatic URL parameter extraction and pattern matching
- View Integration: Easy view rendering with
set-view - REST API Support: Return JSON responses with
<rest-controller> - Transaction Support: Transaction management in coordination with Models
- Backward Compatibility: Traditional
do-get/do-poststyle and new action-based style can coexist
For detailed API reference, please refer to the docstring of each function.