kemal-controller
Version, currently 0.2.05 versions
- 0.3.0latestAug 2, 2026
- 0.2.0not indexedAug 14, 2026
- 0.1.2not indexedAug 14, 2026
- 0.1.1not indexedAug 14, 2026
- 0.1.0not indexedAug 14, 2026
github.com/hugopl/kemal-controller
Controller classes for your kemal application.
7 stars
0 dependents
License: MIT
Nothing has been indexed for 0.2.0 yet. The tag is recorded, its shard.yml has not been read, so the manifest and dependency list below are empty because they are unknown rather than because they are absent.
Installation
# Add this to your shard.yml
dependencies:
kemal-controller:
github: hugopl/kemal-controller
version: ~> 0.2.0Then run:
shards installshard.yml
No shard.yml has been indexed for 0.2.0. You can read it on the repository.
Dependencies
Unknown: the shard.yml for this version has not been read yet.
README
This README is the one indexed from the repository at its latest ref, not from the tag for this version.
# 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.2.0- Tagged
- Aug 14, 2026
- Commit
75a6b65b7343- Indexed
- not yet
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