kemal-controller
Version, currently 0.3.05 versions
github.com/hugopl/kemal-controller
Controller classes for your kemal application.
7 stars
0 dependents
License: MIT
Installation
# Add this to your shard.yml
dependencies:
kemal-controller:
github: hugopl/kemal-controller
version: ~> 0.3.0Then run:
shards installshard.yml
- Crystal
>= 1.19.1- License
- MIT
- Author
- Hugo Parente Lima <hugo.pl@gmail.com>
Dependencies
Runtime Dependencies
- kemal>= 1.10.1github: kemalcr/kemal
Development Dependencies
- spec-kemal*github: kemalcr/spec-kemaldev
- kemal-session*github: kemalcr/kemal-sessiondev
README
# kemal-controller
Kemal is awesome, but sometimes you need (or just want) a bit more structure in
your web applications, kemal-controller is here to help you with that by
providing a simple way to declare all your endpoints into controller classes
where the method parameters will map to GET/PATCH/.../POST/URL parameters automatically.
Online documentation can be found at: <https://hugopl.github.io/kemal-controller/>.
Controllers are structs, so the overhead is minimal and you can still use all
Kemal features as you would normally do.
```Crystal
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} and description: #{description}"
end
end
```
Kemal-controller also supports arrays and named tuples in arguments, so you get a type safe way to handle the endpoint parameters.
```Crystal
struct ProductsController < Kemal::Controller
@[Get("/products")]
def filter(categories : Array(String), price_range : NamedTuple(min : Float64, max : Float64))
"Filtering products in categories: #{categories.join(", ")} with price between #{price_range[:min]} and #{price_range[:max]}"
end
end
```
It supports nested named tuples/arrays in any combination as well.
```Crystal
struct OrdersController < Kemal::Controller
@[Post("/orders")]
def create(items : Array(NamedTuple(id : Int32, quantity : Int32)),
shipping_address : NamedTuple(street : String, city : String, zip : String))
"Creating order with items: #{items.inspect} to be shipped to #{shipping_address[:street]}, #{shipping_address[:city]}, #{shipping_address[:zip]}"
end
end
```
Default values are supported — the parameter must have an explicit type annotation:
```Crystal
struct UsersController < Kemal::Controller
@[Get("/greet")]
def greet(name : String = "World", times : Int32 = 1)
"Hello, #{name}! " * times
end
end
```
If the parameter is absent from the request the default value is used. Explicit type annotations are required; omitting the type is a compile-time error.
### How the parameters are mapped?
Kemal-controller interprets the form keys almost like Rails does:
- `item[foo]=bar` becomes `item : NamedTuple(foo : String)`
- `items[]=1&items[]=2` becomes `items : Array(Int32)`
- `items[][id]=1&items[][quantity]=2&items[][id]=3&items[][quantity]=4` becomes `items : Array(NamedTuple(id : Int32, quantity : Int32))`
- `name=John` becomes `name : String`
### Supported types
- String
- Int32
- Int64
- Enums
- Bool
- NamedTuple (with nested support)
- Array (with nested support)
- Nilable versions of the above types
More types may be added in the future, feel free to open an issue or a PR if you need something specific.
### Error handling
Two exceptions are raised for bad request parameters:
- `Kemal::MissingParameterError` — a required (non-nilable, no default) parameter was not present in the request.
- `Kemal::InvalidParameterError` — the parameter was present but its value could not be coerced to the declared type (e.g. `"foo"` for an `Int32`, an unrecognised enum member, or an invalid boolean literal).
Both inherit from `Exception`. You can handle them with Kemal's exception-specific error handlers:
```Crystal
error Kemal::MissingParameterError do |env, ex|
env.response.status_code = 400
ex.message
end
error Kemal::InvalidParameterError do |env, ex|
env.response.status_code = 422
ex.message
end
```
### Enums
Enums are supported as method parameters as well, anything accepted by `Enum.new` or `Enum.parse` is recognized.
### Stripping parameters
If you need to strip all parameters (like leading/trailing spaces) before they
reach your controller methods, you can use the `strip` flag on method annotation.
To strip specific parameters use an array of symbols instead of true.
```Crystal
struct UsersController < Kemal::Controller
@[Post("/users", strip: true)]
def create(name : String, description : String?)
"Creating user with name: '#{name}', description: '#{description}'"
end
@[Get("/users/edit", strip: [:email])]
def login(email : String, password : String)
"Logging in user with email: '#{email}'"
end
end
```
### Authenticated/protected routes
If you need to protect some routes with authentication you must set the `auth`
flag to true in the method annotation and implement the `authenticate! : Bool`
method in your controller.
If `authenticate!` returns false the request will be halted and no further
processing will be done, status code is set to 401 (Unauthorized).
```Crystal
struct AdminController < Kemal::Controller
@[Get("/admin/dashboard", auth: true)]
def dashboard
"Welcome to the admin dashboard!"
end
def authenticate! : Bool
if !current_user.try(&.current_user.admin?)
redirect("/login")
return false
end
true
end
end
```
### WebSocket routes
WebSocket endpoints are declared with `@[WebSocket]`, taking advantage of Kemal's
own WebSocket support. The method is called once, right after the handshake
completes; use the `socket` getter to register `on_message`/`on_close`/etc.
handlers. Parameters are extracted from the handshake request the same way `Get` does.
```Crystal
struct ChatController < Kemal::Controller
@[WebSocket("/chat/:room")]
def chat(room : String)
socket.send("Welcome to #{room}!")
socket.on_message do |message|
socket.send("#{room}: #{message}")
end
end
end
```
`strip` works the same as with HTTP routes. `auth` works too, but with one
difference: by the time the method runs the handshake response has already
been sent, so a failed `authenticate!` can't reply with a 401 — the socket is
closed instead with `HTTP::WebSocket::CloseCode::PolicyViolation`.
### Printing routes
You can print all registered routes by calling the `Kemal.print_routes` method,
useful for debugging purposes.
```Crystal
Kemal.config.extra_options do |parser|
parser.on("--routes", "Show all routes") do
Kemal.print_routes
exit(0)
end
end
```
On `--routes` your app will print something like:
```
GET /area51 TestController#area51()
POST /array_of_named_tuples TestController#array_of_named_tuples(items : Array(NamedTuple(name: String, age: Int32)))
GET /hello TestController#hello(name : String)
POST /hello TestController#post_hello(name : String)
GET /regular_kemal_route ?
WS /chat/:room ChatController#chat(room : String)
5 routes
```
## Installation
1. Add the dependency to your `shard.yml`:
```yaml
dependencies:
kemal-controller:
github: hugopl/kemal-controller
```
2. Run `shards install`
## Contributing
1. Fork it (<https://github.com/hugopl/kemal-controller/fork>)
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create a new Pull Request
## Contributors
- [Hugo Parente Lima](https://github.com/hugopl) - creator and maintainer
Documentation
Built from the current release. The first visit to a release nobody has asked for starts its build.
Links
This release
- Version
0.3.0- Tagged
- Aug 2, 2026
- Commit
fc80c25a5278- Crystal
>= 1.19.1- Indexed
- yes
Dependents
No indexed shard depends on this one yet.
Repository
github.com/hugopl/kemal-controller
Metadata
- Created
- Aug 12, 2026
- Updated
- Aug 14, 2026
- Synced
- Aug 14, 2026
- Versions
- 5