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)
- 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...
))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
- 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
For detailed API reference, please refer to the docstring of each function.