abstract struct Kemal::Controller

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

Parameter Mapping

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

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

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.cr

Constructors

Instance Method Summary

Constructor Detail

def self.new(context : HTTP::Server::Context, socket : HTTP::WebSocket | Nil = nil) #

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

  • #context : HTTP::Server::Context - The HTTP server context for the request
  • #socket : HTTP::WebSocket? - The WebSocket connection, only set for @[WebSocket] methods

[View source]

Instance Method Detail

def close(*args, **options) #

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.


[View source]
def close(*args, **options, &) #

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.


[View source]
def context : HTTP::Server::Context #

The HTTP server context for the current request.

Provides access to the underlying HTTP::Server::Context which contains the request and response objects.


[View source]
def error(field, message, status : HTTP::Status | Nil = nil) #

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 error
  • message : String - The error message for this field
  • status : 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

[View source]
def error(message : String) #

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

[View source]
def error_for?(field : String) : String | Nil #

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

[View source]
def error_for_base : String | Nil #

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

[View source]
def errors : Errors | Nil #

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.


[View source]
def has_error? : Bool #

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

[View source]
def on_binary(&block : Bytes -> ) : Proc(Bytes, Nil) #

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.


[View source]
def on_close(&block : HTTP::WebSocket::CloseCode, String -> ) : Proc(HTTP::WebSocket::CloseCode, String, Nil) #

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.


[View source]
def on_message(&block : String -> ) : Proc(String, Nil) #

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.


[View source]
def on_ping(&block : String -> ) #

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.


[View source]
def on_pong(&block : String -> ) #

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.


[View source]
def ping(*args, **options) #

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.


[View source]
def ping(*args, **options, &) #

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.


[View source]
def pong(*args, **options) #

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.


[View source]
def pong(*args, **options, &) #

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.


[View source]
def redirect(*args, **options) #

Delegates to the redirect method from the context.

Redirects the request to another URL.

Example

redirect("/login")

[View source]
def redirect(*args, **options, &) #

Delegates to the redirect method from the context.

Redirects the request to another URL.

Example

redirect("/login")

[View source]
def request(*args, **options) #

Delegates to the request object from the context.

Provides direct access to the HTTP::Request for the current request.


[View source]
def request(*args, **options, &) #

Delegates to the request object from the context.

Provides direct access to the HTTP::Request for the current request.


[View source]
def response(*args, **options) #

Delegates to the response object from the context.

Provides direct access to the HTTP::Response for the current request.


[View source]
def response(*args, **options, &) #

Delegates to the response object from the context.

Provides direct access to the HTTP::Response for the current request.


[View source]
def send(*args, **options) #

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.


[View source]
def send(*args, **options, &) #

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.


[View source]
def session(*args, **options) #

Delegates to the session object from the context.

Provides access to the Kemal session for the current request.


[View source]
def session(*args, **options, &) #

Delegates to the session object from the context.

Provides access to the Kemal session for the current request.


[View source]
def socket : HTTP::WebSocket #

The WebSocket connection for the current request.

Only available inside methods annotated with @[WebSocket]. Raises NilAssertionError if accessed from a regular HTTP route handler.


[View source]
def socket? : HTTP::WebSocket | Nil #

The WebSocket connection for the current request.

Only available inside methods annotated with @[WebSocket]. Raises NilAssertionError if accessed from a regular HTTP route handler.


[View source]
def stream(binary = true, frame_size = 1024, &) #

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.


[View source]