Controllers
Authenticated Lesli engine controllers inherit shared request, response, customization, security, and audit behavior from Lesli::ApplicationLesliController.
Define one application controller inside the engine namespace, then inherit resource controllers from it:
module MyEngine
class ApplicationController < Lesli::ApplicationLesliController
end
class TicketsController < ApplicationController
end
end
Use Lesli::ApplicationController instead for public pages that do not need the authenticated engine lifecycle.
Controller Lifecycle
Lesli::ApplicationLesliController runs the following callbacks:
- Select the locale from the session or
Require-Languagerequest header. - Authenticate and authorize the request when LesliShield is installed.
- Prepare customization state.
- Normalize common query parameters into
query. - Log the completed request when LesliAudit is installed.
It also uses the lesli/layouts/application-lesli layout and includes the requester and responder interfaces described in Controller Interfaces.
Security and audit callbacks are conditional. Inheriting from this controller does not install LesliShield or LesliAudit.
Query Parameters
The requester interface exposes normalized request options through the query reader:
{
search: params[:search],
pagination: {
perPage: params[:perPage]&.to_i || 12,
page: params[:page]&.to_i || 1
},
order: {
by: params[:orderBy] || "id",
dir: params[:order] || "desc"
}
}
Pass this hash into service objects instead of repeating pagination and ordering parsing in every controller:
def index
scope = TicketService.new(current_user, query).index
@tickets = respond_with_pagination(scope)
end
The service remains responsible for allowlisting sortable columns and safe order directions before using request-derived values in a database query.
Multi-format Responses
Use respond_with_lesli when an action supports HTML, JSON, or Turbo Stream requests:
def update
ticket = TicketService.new(current_user, query).find(id: params[:id])
return respond_with_not_found unless ticket.found?
if ticket.update(ticket_params)
respond_with_lesli(
turbo: stream_notification_success("Ticket updated"),
json: ticket.result,
redirect: ticket_path(ticket.result)
)
else
respond_with_lesli(
turbo: stream_notification_danger(ticket.errors_as_sentence),
json: { errors: ticket.errors },
html: "my_engine/tickets/edit",
status: :unprocessable_entity
)
end
end
html: identifies a template or renderable, while redirect: identifies an HTML redirect destination. Do not pass the result of redirect_to through html: and do not provide both options in one call. When neither is present, Rails renders the current action's HTML template.
status: applies to HTML, JSON, and Turbo Stream renders and defaults to :ok. HTML redirects use redirect_status: :see_other by default. Only provide formats the action is expected to serve; the responder selects the payload matching the request format.
See Controller Interfaces for complete examples covering multiple Turbo Stream actions, redirects, JSON payloads, default HTML rendering, and validation errors.
Pagination Responses
respond_with_pagination expects a paginated collection that responds to Kaminari's pagination methods:
records = Ticket
.order(id: :desc)
.page(query[:pagination][:page])
.per(query[:pagination][:perPage])
payload = respond_with_pagination(records)
The returned hash has this shape:
{
pagination: {
page: 1,
pages: 4,
total: 42,
results: 12
},
records: records
}
Passing an ordinary, unpaginated Active Record relation raises an error because the required pagination methods are absent.
Errors and Notifications
The shared controller provides:
respond_with_not_found(message = nil)for HTML, JSON, and Turbo Stream 404 responsesrespond_with_forbidden(details = {})for authorization failures after a user has authenticatedinfo,success,warning, anddangerfor flash messagesstream_notification_info,stream_notification_success,stream_notification_warning, andstream_notification_dangerfor Turbo Stream notificationsstream_redirection(path)for redirects initiated from a Turbo Stream response
Authorization failures return HTTP 403. respond_with_unauthorized remains available as a backwards-compatible alias, but new code should use respond_with_forbidden. Authentication failures are separate: an unauthenticated browser request is handled by LesliShield's authentication interface and redirected to login.
Avoid returning authorization details in production. The built-in forbidden responder suppresses its diagnostic details when Rails runs in production.
Strong Parameters
Use standard Rails strong parameters at the controller boundary:
private
def ticket_params
params.require(:ticket).permit(:subject, :description, :priority_id)
end
Keep required parameter names aligned with the form and API payload.
Last Update: 2026/10/13