abstract struct
Kemal::Controller
- Kemal::Controller
- Struct
- Value
- Object
Overview
Abstract controller class that provides a structured way to define HTTP endpoints.
Controllers are structs, so the overhead is minimal and you can still use all Kemal features. Method parameters automatically map to GET/POST/URL parameters with type-safe conversion.
Example
struct UsersController < Kemal::Controller
@[Get("/users")]
def index
"Listing all users"
end
@[Get("/users/:id")]
def show(id : Int32)
"Showing user with ID: #{id}"
end
@[Post("/users")]
def create(name : String, age : Int32, description : String?)
"Creating user with name: #{name}, age: #{age}, description: #{description}"
end
end
Supported Parameter Types
- String
- Int32, Int64
- Bool
- Array (with nested support)
- NamedTuple (with nested support)
- Hash with String keys (with nested support)
- Nilable versions of the above
Parameter Mapping
name=Johnbecomesname : Stringitem[foo]=barbecomesitem : NamedTuple(foo: String)items[]=1&items[]=2becomesitems : Array(Int32)items[][id]=1&items[][quantity]=2becomesitems : Array(NamedTuple(id: Int32, quantity: Int32))opts[width]=800&opts[height]=600becomesopts : Hash(String, Int32), for keys only known at runtime
A parameter's external name (the one looked up in the request) can differ from the name used in the
method body by giving it an internal name, same as any other Crystal method. This is required when the
request field name is a reserved word, e.g. def sign_in(next url : String) maps the next request
parameter to the local variable url.
Route annotation parameters
path: String - The URL path for the route (can include path parameters like:id)auth: Bool - If true, requires authentication viaauthenticate!method (default: false). Must be set explicitly when compiled with thekemal_controller_require_authflag.strip: Bool | Array(Symbol) - If true, strips all parameters; if array, strips only specified parameters (default: false)status: Int32 - The HTTP status code to set before the action runs (default: 200). The action can still override it, e.g. by calling#error.as: Symbol - The name of the route's URL helper inKemal::Routes(default:{controller}_{action})
Example
@[Get("/users/:id")]
def show(id : Int32)
"User #{id}"
end
Example with a custom status code
@[Post("/users", status: 201)]
def create(name : String)
"Creating user with name: #{name}"
end
Example with Authentication
@[Get("/admin/dashboard", auth: true)]
def dashboard
"Admin Dashboard"
end
def authenticate! : Bool
# Return false to halt with 401 status
request.headers["Authorization"]? == "SecretToken"
end
authenticate! can also take over the response itself, e.g. to redirect instead of
replying with 401:
def authenticate! : Bool
return true if session.string?("user")
redirect("/login")
false
end
kemal-controller only sets the 401 status when authenticate! returns false and hasn't
already changed the response's status code or added a response header (e.g. via #redirect,
response.status_code =, or setting a new response header directly). This still works when
authenticate! responds with the same status the response already had (e.g. it wants to
reply 200 with an HX-Redirect header for an htmx request), since a header was added.
Overwriting the value of a header that was already present, without changing the status or
adding a new header, isn't detected as taking over the response.
Example with before_all Filters
before_all registers methods to run before every route declared in the same controller
struct, in declaration order. Filters run after authenticate! and after the route's
status: has been applied, but before any parameter is parsed. Call halt from a filter
(or from an action) to abort the request.
struct AdminController < Kemal::Controller
before_all :load_current_user
before_all :require_admin
@[Get("/admin/dashboard")]
def dashboard
"Welcome, #{@user}"
end
private def load_current_user
@user = session.string?("user")
end
private def require_admin
halt(403, "Forbidden") unless @user == "admin"
end
end
Example with Parameter Stripping
@[Post("/users", strip: true)]
def create(name : String, description : String?)
# name and description will have leading/trailing whitespace removed
end
@[Post("/login", strip: [:email])]
def login(email : String, password : String)
# Only email will be stripped, password remains unchanged
end
Example with a Cast-Error Hook
By default, if a required parameter is missing, or is present but fails to
cast to its declared type (e.g. age=foo for age : Int32),
Kemal::ParamError propagates uncaught. Define an opt-in
{action}_on_cast_error method, with the same parameters as the action but
with no type restrictions, to render a response instead:
@[Post("/users")]
def create(name : String, age : Int32)
"Creating user with name: #{name}, age: #{age}"
end
def create_on_cast_error(name, age)
# Both `name` and `age` are unions with `Kemal::ParamError`, since
# either can be missing, and `age` can also fail to cast.
if age.is_a?(Kemal::ParamError)
age.reason.missing? ? "age is required" : "age: #{age.value.inspect} is not a number"
else
"age was fine: #{age}"
end
end
Defined in:
kemal/controller.crConstructors
-
.new(context : HTTP::Server::Context, socket : HTTP::WebSocket | Nil = nil)
Initializes a new controller instance.
Macro Summary
-
before_all(*names)
Registers methods to run before every route declared in this controller.
-
halt(status_code = 200, response = "")
Aborts the current request by raising
Halt.
Instance Method Summary
-
#_run_before_all_filters : Nil
Runs this controller's
before_allfilters. -
#close(*args, **options)
Delegates the non-block WebSocket methods to the
#socketgetter. -
#close(*args, **options, &)
Delegates the non-block WebSocket methods to the
#socketgetter. -
#context : HTTP::Server::Context
The HTTP server context for the current request.
-
#error(field, message, status : HTTP::Status | Nil = nil)
Adds a field-specific error message.
-
#error(message : String)
Adds a general error message to the base error field.
-
#error_for?(field : String) : String | Nil
Returns the error message for a specific field.
-
#error_for_base : String | Nil
Returns the error message for the "base" field.
-
#errors : Errors | Nil
Hash of validation errors that occurred during request processing.
-
#has_error? : Bool
Checks if any errors have been recorded.
-
#on_binary(&block : Bytes -> ) : Proc(Bytes, Nil)
Forwards the block-accepting WebSocket methods to the
#socketgetter. -
#on_close(&block : HTTP::WebSocket::CloseCode, String -> ) : Proc(HTTP::WebSocket::CloseCode, String, Nil)
Forwards the block-accepting WebSocket methods to the
#socketgetter. -
#on_message(&block : String -> ) : Proc(String, Nil)
Forwards the block-accepting WebSocket methods to the
#socketgetter. -
#on_ping(&block : String -> )
Forwards the block-accepting WebSocket methods to the
#socketgetter. -
#on_pong(&block : String -> )
Forwards the block-accepting WebSocket methods to the
#socketgetter. -
#ping(*args, **options)
Delegates the non-block WebSocket methods to the
#socketgetter. -
#ping(*args, **options, &)
Delegates the non-block WebSocket methods to the
#socketgetter. -
#pong(*args, **options)
Delegates the non-block WebSocket methods to the
#socketgetter. -
#pong(*args, **options, &)
Delegates the non-block WebSocket methods to the
#socketgetter. -
#redirect(*args, **options)
Delegates to the redirect method from the context.
-
#redirect(*args, **options, &)
Delegates to the redirect method from the context.
-
#request(*args, **options)
Delegates to the request object from the context.
-
#request(*args, **options, &)
Delegates to the request object from the context.
-
#response(*args, **options)
Delegates to the response object from the context.
-
#response(*args, **options, &)
Delegates to the response object from the context.
-
#send(*args, **options)
Delegates the non-block WebSocket methods to the
#socketgetter. -
#send(*args, **options, &)
Delegates the non-block WebSocket methods to the
#socketgetter. -
#session(*args, **options)
Delegates to the session object from the context.
-
#session(*args, **options, &)
Delegates to the session object from the context.
-
#socket : HTTP::WebSocket
The WebSocket connection for the current request.
-
#socket? : HTTP::WebSocket | Nil
The WebSocket connection for the current request.
-
#stream(binary = true, frame_size = 1024, &)
Forwards the block-accepting WebSocket methods to the
#socketgetter.
Constructor Detail
Initializes a new controller instance.
This is called automatically by the framework when processing a request. You typically don't need to call this directly.
Parameters
Macro Detail
Registers methods to run before every route declared in this controller.
Accepts symbols, bare names or strings, and may be called more than once; filters run in declaration order, and only for routes declared in this controller struct. It can appear anywhere in the struct body, before or after the routes it applies to.
Filters run after authenticate! (for auth: true routes) and after the route's
status: has been applied, but before any parameter is parsed or cast. Their return
value is ignored; call halt to abort the request.
Since a controller is a struct that is instantiated per request, a filter can assign instance variables for the action to read.
Example
struct PostsController < Kemal::Controller
before_all :load_current_user
before_all :require_admin
@[Get("/posts")]
def index
"Hello #{@user}"
end
private def load_current_user
@user = session.string?("user")
end
private def require_admin
halt(403, "Forbidden") unless @user == "admin"
end
end
Aborts the current request by raising Halt.
Usable from before_all filters, from actions, and from any other method of the
controller. The action (and any remaining filters) are skipped.
NOTE This shadows Kemal's own top-level halt macro inside controllers, and takes no
env/context argument. Kemal's version expands to next, so it never worked inside a
controller method to begin with.
Example
before_all :ensure_setup
def ensure_setup
halt(403, "Forbidden") unless Config.ready?
end
Instance Method Detail
Runs this controller's before_all filters.
No-op by default; controllers that call before_all get an override generated for them.
:nodoc:
Delegates the non-block WebSocket methods to the #socket getter.
Lets @[WebSocket] methods call #send, #close, etc. directly instead of
going through #socket. Like #socket, these raise NilAssertionError if
called from a regular HTTP route handler.
Delegates the non-block WebSocket methods to the #socket getter.
Lets @[WebSocket] methods call #send, #close, etc. directly instead of
going through #socket. Like #socket, these raise NilAssertionError if
called from a regular HTTP route handler.
The HTTP server context for the current request.
Provides access to the underlying HTTP::Server::Context which contains the request and response objects.
Adds a field-specific error message.
Stores an error message for a specific field and sets the appropriate HTTP status code. If no custom status is provided, sets 400 (Bad Request) for GET/HEAD/OPTIONS requests or 422 (Unprocessable Entity) for POST/PUT/PATCH/DELETE requests.
Parameters
field: String - The name of the field that has an errormessage: String - The error message for this fieldstatus: HTTP::Status? - Optional custom HTTP status code (default: nil)
Example
def create(email : String, password : String)
if !email.includes?("@")
error("email", "Invalid email format")
render("src/views/users/new.ecr")
return
end
if password.size < 8
error("password", "Password must be at least 8 characters", HTTP::Status::BAD_REQUEST)
render("src/views/users/new.ecr")
return
end
end
Adds a general error message to the base error field.
This is useful for errors that don't belong to a specific field. Sets the response status to 400 (Bad Request) for GET/HEAD/OPTIONS requests or 422 (Unprocessable Entity) for POST/PUT/PATCH/DELETE requests.
Parameters
message: String - The error message to add
Example
def create(name : String)
if name.empty?
error("Name cannot be empty")
render("src/views/users/new.ecr")
return
end
end
Returns the error message for a specific field.
Returns nil if there is no error for the specified field.
Parameters
field: String - The name of the field to check for errors
Example
def create(email : String)
error("email", "Invalid email") unless email.includes?("@")
if msg = error_for?("email")
render("src/views/users/new.ecr")
return
end
end
Returns the error message for the "base" field.
The "base" field is used for general errors that don't belong to a specific field.
Returns nil if there is no base error.
Example
def update
error("Something went wrong")
if msg = error_for_base
render("src/views/error.ecr")
return
end
end
Hash of validation errors that occurred during request processing.
Maps field names to error messages. Use #error methods to add errors
and #has_error?, #error_for?, #error_for_base to check for errors.
Returns nil if no errors have been recorded.
Checks if any errors have been recorded.
Returns true if there are one or more validation errors, false otherwise.
Example
def create(name : String, email : String)
error("name", "Name is required") if name.empty?
error("email", "Email is required") if email.empty?
if has_error?
render("src/views/users/new.ecr")
return
end
# Process the valid data
end
Forwards the block-accepting WebSocket methods to the #socket getter.
These can't be handled by delegate because the target methods capture their
block (&), and the wrapper delegate generates would yield from inside a
captured block, which doesn't compile. Like #socket, they raise
NilAssertionError when called from a regular HTTP route handler.
Forwards the block-accepting WebSocket methods to the #socket getter.
These can't be handled by delegate because the target methods capture their
block (&), and the wrapper delegate generates would yield from inside a
captured block, which doesn't compile. Like #socket, they raise
NilAssertionError when called from a regular HTTP route handler.
Forwards the block-accepting WebSocket methods to the #socket getter.
These can't be handled by delegate because the target methods capture their
block (&), and the wrapper delegate generates would yield from inside a
captured block, which doesn't compile. Like #socket, they raise
NilAssertionError when called from a regular HTTP route handler.
Forwards the block-accepting WebSocket methods to the #socket getter.
These can't be handled by delegate because the target methods capture their
block (&), and the wrapper delegate generates would yield from inside a
captured block, which doesn't compile. Like #socket, they raise
NilAssertionError when called from a regular HTTP route handler.
Forwards the block-accepting WebSocket methods to the #socket getter.
These can't be handled by delegate because the target methods capture their
block (&), and the wrapper delegate generates would yield from inside a
captured block, which doesn't compile. Like #socket, they raise
NilAssertionError when called from a regular HTTP route handler.
Delegates the non-block WebSocket methods to the #socket getter.
Lets @[WebSocket] methods call #send, #close, etc. directly instead of
going through #socket. Like #socket, these raise NilAssertionError if
called from a regular HTTP route handler.
Delegates the non-block WebSocket methods to the #socket getter.
Lets @[WebSocket] methods call #send, #close, etc. directly instead of
going through #socket. Like #socket, these raise NilAssertionError if
called from a regular HTTP route handler.
Delegates the non-block WebSocket methods to the #socket getter.
Lets @[WebSocket] methods call #send, #close, etc. directly instead of
going through #socket. Like #socket, these raise NilAssertionError if
called from a regular HTTP route handler.
Delegates the non-block WebSocket methods to the #socket getter.
Lets @[WebSocket] methods call #send, #close, etc. directly instead of
going through #socket. Like #socket, these raise NilAssertionError if
called from a regular HTTP route handler.
Delegates to the redirect method from the context.
Redirects the request to another URL.
Example
redirect("/login")
Delegates to the redirect method from the context.
Redirects the request to another URL.
Example
redirect("/login")
Delegates to the request object from the context.
Provides direct access to the HTTP::Request for the current request.
Delegates to the request object from the context.
Provides direct access to the HTTP::Request for the current request.
Delegates to the response object from the context.
Provides direct access to the HTTP::Response for the current request.
Delegates to the response object from the context.
Provides direct access to the HTTP::Response for the current request.
Delegates the non-block WebSocket methods to the #socket getter.
Lets @[WebSocket] methods call #send, #close, etc. directly instead of
going through #socket. Like #socket, these raise NilAssertionError if
called from a regular HTTP route handler.
Delegates the non-block WebSocket methods to the #socket getter.
Lets @[WebSocket] methods call #send, #close, etc. directly instead of
going through #socket. Like #socket, these raise NilAssertionError if
called from a regular HTTP route handler.
Delegates to the session object from the context.
Provides access to the Kemal session for the current request.
Delegates to the session object from the context.
Provides access to the Kemal session for the current request.
The WebSocket connection for the current request.
Only available inside methods annotated with @[WebSocket]. Raises
NilAssertionError if accessed from a regular HTTP route handler.
The WebSocket connection for the current request.
Only available inside methods annotated with @[WebSocket]. Raises
NilAssertionError if accessed from a regular HTTP route handler.
Forwards the block-accepting WebSocket methods to the #socket getter.
These can't be handled by delegate because the target methods capture their
block (&), and the wrapper delegate generates would yield from inside a
captured block, which doesn't compile. Like #socket, they raise
NilAssertionError when called from a regular HTTP route handler.