Kemal development
Use this skill for Kemal-specific work. Assume normal Crystal language knowledge and follow the Crystal documentation for language semantics that are not specific to Kemal.
Core rules
- Prefer Kemal's own APIs and terminology over Ruby/Sinatra assumptions.
- Match parameters to the request encoding:
env.params.url,.query,.json,.body, and.filesare not interchangeable. - Route definition order matters: the first matching route wins.
- Use
Kemal::Routerand namespaces to structure larger applications. - Use filters for concise route-lifecycle behavior; use middleware for reusable handler-layer concerns and plugins.
- A middleware handler must call
call_nextonly when processing should continue. - Keep business logic out of ECR templates.
- Treat every request-derived value as untrusted until validated.
- Do not pass unchecked user-controlled paths to
send_file. - Configure production WebSocket origins explicitly when browser clients are expected; origin validation is not authentication.
- Add or update specs when route or middleware behavior changes.
- Prefer current Kemal repository APIs over patterns remembered from older versions.
Quick start
require "kemal"
get "/" do
"Hello World!"
end
Kemal.run
Add Kemal to shard.yml:
dependencies:
kemal:
github: kemalcr/kemal
Then:
shards install
crystal run src/my_app.cr
Routing
Kemal supports the common HTTP verbs plus QUERY:
get "/users"
post "/users"
put "/users/:id"
patch "/users/:id"
delete "/users/:id"
query "/search"
Routes are matched in definition order. Check broad/wildcard routes for shadowing before adding more specific routes later.
Dynamic route parameters:
get "/users/:id" do |env|
id = env.params.url["id"]?
env.json({id: id})
end
Wildcard remainder:
get "/files/*all" do |env|
env.params.url["all"]?
end
For QUERY (RFC 10008, since Kemal 1.13.0), use the same Kemal parameter APIs appropriate to the request body. A QUERY request with a body and no Content-Type is rejected with 400 Bad Request according to Kemal's current behavior.
Parameters
Query parameters:
width = env.params.query["width"]?
JSON body (Content-Type: application/json):
name = env.params.json["name"]?.as?(String)
Form/body parameters:
name = env.params.body["name"]?.as?(String)
Repeated/bracketed form keys must be accessed exactly as sent, for example env.params.body["likes[]"]?.
Uploads are available through env.params.files.
Request and response context
Prefer response helpers when they express the result clearly:
get "/users" do |env|
env.json({users: %w[alice bob]})
end
post "/users" do |env|
env.status(:created).json({created: true})
end
Manual response control remains available through env.response for headers, status, and content type.
Use halt inside routes when route execution must stop:
get "/admin" do |env|
halt env.status(:forbidden).html("<h1>Forbidden</h1>")
end
Do not treat halt as generic middleware control flow.
Modular routers
api = Kemal::Router.new
api.namespace "/users" do
get "/" do |env|
env.json({users: %w[alice bob]})
end
end
mount "/api/v1", api
Routers can contain routes, filters, WebSockets, SSE endpoints, and nested namespaces. Keep router-scoped behavior scoped unless global behavior is intentional.
Filters and middleware
Common filters include before_all, verb-specific before_* and matching after_* filters, including before_query / after_query for QUERY routes.
Typical order:
before_all -> before_<verb> -> route -> after_<verb> -> after_all
A HEAD request without its own route is served by the GET route, and it runs that route's filters as well as any registered for HEAD. before_get guards therefore apply to HEAD, and so do Kemal::Handler only / exclude rules scoped to GET.
Reusable cross-cutting behavior belongs in middleware:
class CustomHandler < Kemal::Handler
def call(env)
# before
call_next env
# after, if appropriate
end
end
# Preferred modern registration (Kemal 1.10+):
use CustomHandler.new
# Or configure on Kemal.config:
# Kemal.config.add_handler CustomHandler.new
Do not call call_next after intentionally short-circuiting the request.
Views
Render ECR templates with render and use a second path for layouts. Layout content is available as content; content_for / yield_content can define named slots.
Keep application and authorization logic outside templates.
Security-sensitive behavior
- Validate dynamic
send_filepaths using canonical containment inside an allowed root. - Validate upload presence, size, type/extension as appropriate, and destination path before persistence.
- Use server-controlled upload names when client filenames could affect paths.
- Keep private uploads outside automatically served public directories.
- Set explicit request/multipart size limits appropriate to the application.
- Keep secrets outside source control.
- Avoid leaking stack traces, filesystem paths, database errors, tokens, or session secrets.
- Use HTTPS in production and configure security headers at a deliberate layer.
- Treat WebSocket origin checks as an additional browser boundary, not authentication/authorization.
Testing
Use spec-kemal for application-level Kemal specs:
development_dependencies:
spec-kemal:
github: kemalcr/spec-kemal
require "spec-kemal"
require "../src/my_app"
describe "My App" do
it "renders /" do
get "/"
response.body.should eq "Hello World!"
end
end
Run:
KEMAL_ENV=test crystal spec
Test success and rejection paths, status codes, content types, parameter decoding, route ordering, middleware behavior, authorization, and upload/path validation where relevant.
Load detailed references only when relevant
For more detail, read the smallest relevant reference file instead of loading everything:
- Routing, params, context, routers, views, filters, middleware, errors, static files:
references/http-core.md - Sessions, uploads, WebSockets, SSE, caching:
references/realtime-state.md - Limits, security, configuration, tests, builds, proxies, databases, checklist:
references/operations.md
Verification checklist
Before considering Kemal work complete:
- Route ordering does not shadow expected endpoints.
- Each route uses the intended HTTP verb, including
QUERYwhere applicable. - URL/query/JSON/form/file params use the correct
env.params.*source. - Inputs are validated at trust boundaries.
- Status codes and content types are intentional.
- Router/filter/middleware scope is correct.
- Middleware continues with
call_nextonly when intended. - Dynamic file paths and uploads cannot escape allowed storage roots.
- WebSocket origin policy is explicit when needed and auth is handled separately.
- Request and multipart limits are intentional.
- Relevant
spec-kemaltests pass. - Production build and deployment behavior are verified for the target platform.
Specialized domain skills
For targeted domain implementations with tested patterns from kemal-by-example, see the dedicated domain skills:
- Core Routing & Handlers:
kemal-core - Real-Time WebSockets:
kemal-websocket - Server-Sent Events (SSE):
kemal-sse - JSON APIs & Helpers:
kemal-json - Middleware & HMAC:
kemal-middleware - File Uploads & Security:
kemal-upload - Authentication & Sessions:
kemal-auth - SQLite Database & Models:
kemal-database - OAuth2 Integration:
kemal-oauth - Crecto ORM:
kemal-orm - Views & ECR Templates:
kemal-view
Sources
Treat the current Kemal repository and official Kemal documentation as the source of truth. Use current APIs when this skill conflicts with newer project documentation.