This is the full developer documentation for Kemal. # Start of Kemal documentation # Kemal Fast, Super Simple web framework for Crystal. Kemal is a fast, effective, simple web framework written in Crystal. It is inspired by Sinatra but is much faster due to being built with Crystal. ## Quick Start ```crystal require "kemal" get "/" do "Hello World!" end Kemal.run ``` ## Features - **High-performance by default**: Built on Crystal with a thin abstraction layer so you can serve a large number of requests with low latency and low memory footprint. - **Full REST & HTTP support**: Handle all HTTP verbs (GET, POST, PUT, PATCH, DELETE, etc.) with a straightforward routing DSL. - **WebSocket & real-time**: First-class WebSocket support for building chats, dashboards and other real-time experiences. - **JSON-first APIs**: Native JSON handling makes building JSON APIs and microservices feel natural. - **Static assets made easy**: Serve static files (assets, uploads, SPA bundles) efficiently from the same application. - **Template engine included**: Built-in ECR template engine for server‑rendered HTML when you need it. - **Composable middleware**: Flexible middleware system to add logging, auth, rate limiting, metrics and more. - **Ergonomic request/response API**: Simple access to params, headers, cookies and bodies via a clear context object. - **Session management**: Easy session handling with kemal-session, suitable for production apps. - **Production Ready** - Battle-tested and used in production by many companies ## Use Cases ### Web Applications Build full-featured web applications ranging from small MVPs to large, multi‑module platforms: - Server‑rendered dashboards, admin panels and internal tools - Multi‑page sites with shared layouts and partials using ECR templates - Hybrid apps that combine HTML views with JSON endpoints and background jobs Kemal’s routing, filters and middleware make it easy to structure complex applications while keeping the codebase readable. ### APIs and Microservices Design high‑performance HTTP APIs that are easy to consume and maintain: - REST or JSON‑only backends for SPAs and mobile apps - BFF (Backend‑for‑Frontend) services for React/Vue/Svelte frontends - Internal microservices communicating over HTTP/JSON The lightweight request/response API, built‑in JSON support and test tooling (spec‑kemal) make API development straightforward and predictable. ### Real‑time Applications Use WebSockets to power interactive, low‑latency experiences: - Chat applications and notifications - Live dashboards and monitoring UIs - Collaborative tools (e.g. shared documents, whiteboards) Kemal’s WebSocket primitives let you handle connections, broadcasts, rooms and events without heavy abstractions, while still integrating cleanly with the rest of your HTTP routes and middleware. # Getting Started This guide assumes that you already have Crystal installed. If not, check out the [Crystal installation methods](https://crystal-lang.org/install/) and come back when you're done. ## Installing Kemal First you need to create your application: ```bash crystal init app your_app cd your_app ``` Then add *kemal* to the `shard.yml` file as a dependency. ```yaml dependencies: kemal: github: kemalcr/kemal ``` Finally run `shards` to get the dependencies: ```bash shards install ``` You should see something like this: ``` $ shards install Updating https://github.com/kemalcr/kemal.git Installing kemal (1.10.0) ``` That's it! You're now ready to use Kemal in your application. ## Using Kemal You can do awesome stuff with Kemal. Let's start with a simple example. Just change the content of `src/your_app.cr` to: ```crystal require "kemal" get "/" do "Hello World!" end Kemal.run ``` ## Running Kemal Starting your application is easy. Simply run: ```bash crystal run src/your_app.cr ``` If everything goes well, you should see a message saying that Kemal is running. If you are using Windows, use `http://localhost:3000` or `http://127.0.0.1:3000` instead of `http://0.0.0.0:3000`. ``` [development] Kemal is ready to lead at http://0.0.0.0:3000 2015-12-01 13:47:48 UTC 200 GET / 666µs ``` Congratulations on your first Kemal application! This is just the beginning. # Routes You can handle HTTP methods as easy as writing method names and the route with a code block. Kemal will handle all the hard work. ```crystal # GET - Retrieve data, list resources, show pages get "/" do "Hello World!" end # POST - Create new resources post "/" do "Creating a new resource" end # PUT - Replace entire resource put "/" do "Updating entire resource" end # PATCH - Partially update resource patch "/" do "Updating partial resource" end # DELETE - Remove resources delete "/" do "Deleting resource" end ``` Any **string** returned from a route will output to the browser. Routes are matched in the order they are defined. The first route that matches the request is invoked. ## Kemal::Router (Modular Routing) Kemal provides a modular `Kemal::Router` for organizing routes into namespaces, with scoped filters and WebSocket support. The router can be mounted at any path while keeping the existing DSL fully compatible. ```crystal require "kemal" api = Kemal::Router.new api.namespace "/users" do get "/" do |env| env.json({users: ["alice", "bob"]}) end get "/:id" do |env| env.text "user #{env.params.url["id"]}" end end mount "/api/v1", api Kemal.run ``` In this example, the routes are available at `/api/v1/users` and `/api/v1/users/:id`. You can define filters, WebSocket handlers, and additional namespaces within a router. Filters defined inside a namespace are isolated to that router's routes, while global wildcard filters always execute. Routes are matched in the order they are defined. The first route that matches the request is invoked. # HTTP Parameters When passing data through an HTTP request, you will often need to use query parameters, or post parameters depending on which HTTP method you're using. ## URL Parameters Kemal allows you to use variables in your route path as placeholders for passing data. To access URL parameters, you use `env.params.url`. ```crystal # Matches /hello/kemal get "/hello/:name" do |env| name = env.params.url["name"] "Hello back to #{name}" end # Matches /users/1 get "/users/:id" do |env| id = env.params.url["id"] "Found user #{id}" end # Matches /dir/and/anything/after get "/dir/*all" do |env| all = env.params.url["all"] "Found path #{all}" end ``` ## Query Parameters To access query parameters, you use `env.params.query`. ```crystal # Matches /resize?width=200&height=200 get "/resize" do |env| width = env.params.query["width"] height = env.params.query["height"] "Resizing to #{width}x#{height}" end ``` ## POST / Form Parameters Kemal has a few options for accessing post parameters. You can easily access JSON payload from the parameters, or through the standard post body. For JSON parameters, use `env.params.json`. For body parameters, use `env.params.body`. ```crystal # The request content type needs to be application/json # The payload: {"name": "Serdar", "likes": ["Ruby", "Crystal"]} post "/json_params" do |env| name = env.params.json["name"].as(String) likes = env.params.json["likes"].as(Array) "#{name} likes #{likes.join(",")}" end # Using a standard post body # name=Serdar&likes=Ruby&likes=Crystal post "/body_params" do |env| name = env.params.body["name"].as(String) likes = env.params.body["likes"].as(Array) "#{name} likes #{likes.join(",")}" end ``` **NOTE:** For Array or Hash like parameters, Kemal will group like keys for you. Alternatively, you can use the square bracket notation `likes[]=ruby&likes[]=crystal`. Be sure to access the param name exactly how it was passed. (i.e. `env.params.body["likes[]"]`). ## Override Method (PUT, PATCH, DELETE from Forms) HTML forms only support `GET` and `POST` methods. When you need to use `PUT`, `PATCH`, or `DELETE` in RESTful applications, Kemal's `OverrideMethodHandler` middleware lets you simulate these methods. It reads the `_method` magic parameter from a POST request body and rewrites the request to the corresponding HTTP method. **Important:** This middleware is **not** included in Kemal's default handlers. You must add it explicitly to your application: ```crystal require "kemal" # Add OverrideMethodHandler to the handler chain add_handler Kemal::OverrideMethodHandler::INSTANCE put "/users/:id" do |env| id = env.params.url["id"] "User #{id} updated" end patch "/users/:id" do |env| id = env.params.url["id"] "User #{id} partially updated" end delete "/users/:id" do |env| id = env.params.url["id"] "User #{id} deleted" end Kemal.run ``` In your HTML form, add the `_method` parameter as a hidden field: ```html
``` **Allowed methods:** `PUT`, `PATCH`, `DELETE` **Note:** This middleware consumes `params.body` to read the `_method` parameter. You can ignore this parameter when accessing your form data. # HTTP Request / Response Context Accessing the HTTP request/response context (query parameters, body, content_type, headers, status_code) is super easy. You can use the context returned from the block: ```crystal # Matches /hello/kemal get "/hello/:name" do |env| name = env.params.url["name"] "Hello back to #{name}" end # Response helpers: chainable JSON, HTML, text, XML with HTTP::Status support get "/users" do |env| env.json({users: ["alice", "bob"]}) end post "/users" do |env| env.status(:created).json({id: 1, created: true}) end get "/admin" do |env| halt env.status(403).html("

Forbidden

") end # Manual content type (alternative to helpers) get "/user.json" do |env| user = {name: "Kemal", language: "Crystal"}.to_json env.response.content_type = "application/json" user end # Add headers to your response get "/headers" do |env| env.response.headers["Accept-Language"] = "tr" env.response.headers["Authorization"] = "Token 12345" "Headers set" end # Set response status code get "/status-code" do |env| env.response.status_code = 404 "Not Found" end ``` ## Context Storage Contexts are useful for sharing states between filters and middleware. You can use `context` to store some variables and access them later at some point. Each stored value only exists in the lifetime of request / response cycle. ```crystal before_get "/" do |env| env.set "is_kemal_cool", true end get "/" do |env| is_kemal_cool = env.get "is_kemal_cool" "Kemal cool = #{is_kemal_cool}" end ``` This renders `Kemal cool = true` when a request is made to `/`. If you prefer a safer version use `env.get?` which won't raise when the key doesn't exist and will return `nil` instead. ```crystal get "/" do |env| non_existent_key = env.get?("non_existent_key") # => nil end ``` Context storage also supports custom types. You can register and use a custom type as the following: ```crystal class User property name : String def initialize(@name : String) end end add_context_storage_type(User) before "/" do |env| env.set "user", User.new("dummy-user") end get "/" do |env| user = env.get "user" "Hello #{user.name}" end ``` Be aware that you have to declare the custom type before trying to add with `add_context_storage_type`. ## Request Properties Some common request information is available at `env.request.*`: - **method** - the HTTP method (e.g. `GET`, `POST`, ...) - **headers** - a hash containing relevant request header information - **body** - the request body - **version** - the HTTP version (e.g. `HTTP/1.1`) - **path** - the uri path (e.g. `http://kemalcr.com/docs/context?lang=cr` => `/docs/context`) - **resource** - the uri path and query parameters (e.g. `http://kemalcr.com/docs/context?lang=cr` => `/docs/context?lang=cr`) - **cookies** - e.g. `env.request.cookies["cookie_name"].value` # Views / Templates You can use ERB-like built-in [ECR](http://crystal-lang.org/api/ECR.html) to render dynamic views. ```crystal get "/:name" do |env| name = env.params.url["name"] render "src/views/hello.ecr" end ``` Your `hello.ecr` view should have the same context as the method. ```erb Hello <%= name %> ``` ## Using Layouts You can use **layouts** in Kemal. You can do this by passing a second argument to the `render` method. ```crystal get "/:name" do render "src/views/subview.ecr", "src/views/layouts/layout.ecr" end ``` In your layout file, you need to return the output of `subview.ecr` with the `content` variable (like `yield` in Rails). ```erb My Kemal Application <%= content %> ``` ## content_for and yield_content You can capture blocks inside views to be rendered later during the request with the `content_for` helper. The most common use is to populate different parts of your layout from your view. ### Usage First, call `content_for`, generally from a view, to capture a block of markup with an identifier: ```erb # index.ecr <% content_for "some_key" do %> ... <% end %> ``` Then, call **yield_content** with that identifier, generally from a layout, to render the captured block: ```erb # layout.ecr <%= yield_content "some_key" %> ``` This is useful because some of your views may need specific JavaScript tags or stylesheets and you don't want to use these tags in all of your pages. ## Using Common Paths Since Crystal does not allow using variables in macro literals, you need to generate another *helper macro* to make the code easier to read and write. ```crystal macro my_renderer(filename) render "my/app/view/base/path/#{ {{filename}} }.ecr", "my/app/view/base/path/layouts/layout.ecr" end ``` And now you can use your new renderer. ```crystal get "/:name" do my_renderer "subview" end ``` # Filters Before filters are evaluated before each request within the same context as the routes. They can modify the request and response. _Important note: This should **not** be used by plugins/addons, instead they should do all their work in their own middleware._ Available filters: - before_all, before_get, before_post, before_put, before_patch, before_delete - after_all, after_get, after_post, after_put, after_patch, after_delete The `Filter` middleware is lazily added as soon as a call to `after_X` or `before_X` is made. It will not even be instantiated unless a call to `after_X` or `before_X` is made. When using `before_all` and `after_all` keep in mind that they will be evaluated in the following order: ``` before_all -> before_x -> X -> after_x -> after_all ``` ## Simple before_get example ```crystal before_get "/foo" do |env| puts "Setting response content type" env.response.content_type = "application/json" end get "/foo" do |env| puts env.response.headers["Content-Type"] # => "application/json" {"name": "Kemal"}.to_json end ``` ## Simple before_all example `before_all` applies to all HTTP methods for the given path. Same as `before_get` but also runs for `put`, `post`, etc.: ```crystal before_all "/foo" do |env| puts "Setting response content type" env.response.content_type = "application/json" end get "/foo" do |env| puts env.response.headers["Content-Type"] # => "application/json" {"name": "Kemal"}.to_json end put "/foo" do |env| {"name": "Kemal"}.to_json end post "/foo" do |env| {"name": "Kemal"}.to_json end ``` ## Multiple before_all You can add many blocks to the same verb/path combination by calling it multiple times. They will be called in the same order they were defined. ```crystal before_all do |env| raise "Unauthorized" unless authorized?(env) end before_all do |env| env.session = Session.new(env.cookies) end get "/foo" do |env| "foo" end ``` Each time `GET /foo` (or any other route since we didn't specify a route for these blocks) is called the first `before_all` will run and then the second will set the session. Note: `authorized?` and `Session.new` are fictitious calls used to illustrate the example. # Helpers ## Browser Redirect Browser redirects are simple. Simply call `env.redirect` in the route's corresponding block. ```crystal get "/logout" do |env| # important stuff like clearing session etc. env.redirect "/login" # redirect to /login page end ``` ## Halt Halt execution with the current context. Returns 200 and an empty response by default. ```crystal halt env, status_code: 403, response: "Forbidden" ``` You can also halt from a chained response for concise API error handling: ```crystal get "/admin" do |env| halt env.status(403).html("

Forbidden

") end ``` *Note:* `halt` can only be used inside routes. ## Custom Errors You can customize the built-in error pages or even add your own with `error`. ```crystal error 404 do "This is a customized 404 page." end error 403 do "Access Forbidden!" end ``` To handle a custom error based on a raised exception, you pass the exception to `error`: ```crystal get "/" do |env| if some_condition raise ValueError.new end {"message": "Hello Kemal"}.to_json end error ValueError do "Something has gone wrong" end ``` **NOTE** Exception handlers are resolved based on definition order first, and inheritance order second. For example: ```crystal class GrandParentException < Exception; end class ParentException < GrandParentException; end class ChildException < ParentException; end error GrandParentException do "Grandparent exception" end error ParentException do "Parent exception" end get "/" do raise ChildException.new() end ``` Will resolve to the handler for `GrandParentException` rather than `ParentException`. ## Send File Send a file with the given path and base the MIME type on the file extension or default to `application/octet-stream`. ```crystal send_file env, "./path/to/file.jpg" ``` Optionally, you can override the MIME type: ```crystal send_file env, "./path/to/file.exe", "image/jpeg" ``` MIME type detection is based on the [MIME](https://crystal-lang.org/api/MIME.html) registry from the Crystal standard library, which uses the OS-provided MIME database. If unavailable, it falls back to a basic type list ([MIME::DEFAULT_TYPES](https://crystal-lang.org/api/MIME.html#DEFAULT_TYPES)). You can extend the registered type list by calling `MIME.register`: ```crystal MIME.register ".cr", "text/crystal" ``` > **Security Notice:** > When using `send_file` with dynamic file paths (such as those based on user input), always **sanitize and validate** the path to prevent directory traversal and unauthorized file access. Never pass unchecked user input directly to `send_file`. > For example, ensure the path is within an allowed directory and does not contain sequences like `../` that could escape the intended folder. > See [kemalcr/kemal#718](https://github.com/kemalcr/kemal/issues/718) for more details. # Middleware Middleware, also known as `Handler`s, are the building blocks of Kemal. Middleware lets you separate application concerns into different layers. Each middleware is supposed to have one responsibility. ## The `use` Keyword Use the `use` keyword to register middleware globally or for specific paths. You can pass a single handler, an array of handlers, or insert at a specific position in the handler chain. ```crystal require "kemal" # Path-specific middlewares for /api routes use "/api", [CORSHandler.new, AuthHandler.new] get "/" do "Public home" end get "/api/users" do |env| env.json({users: ["alice", "bob"]}) end Kemal.run ``` Global middleware runs for all routes; path-specific middleware runs only for routes matching the given path prefix. ## Creating your own middleware You can create your own middleware by inheriting from `Kemal::Handler` ```crystal class CustomHandler < Kemal::Handler def call(context) puts "Doing some custom stuff here" call_next context end end add_handler CustomHandler.new ``` ## Conditional Middleware Execution Kemal gives you access to two handy filters `only` and `exclude`. These can be used to process your custom middleware for `only` specific routes, or to `exclude` from specific routes. ```crystal class OnlyHandler < Kemal::Handler # Matches GET /specials and GET /deals only ["/specials", "/deals"] def call(env) return call_next(env) unless only_match?(env) puts "Processing only for /specials and /deals" call_next(env) end end class ExcludeHandler < Kemal::Handler # Matches GET / exclude ["/"] def call(env) return call_next(env) if exclude_match?(env) puts "Processing for all routes except /" call_next(env) end end ``` ## Creating a custom Logger middleware You can easily replace the built-in logger of Kemal. There's only one requirement which is that your logger must inherit from `Kemal::BaseLogHandler`. ```crystal class MyCustomLogger < Kemal::BaseLogHandler def call(context) puts "Custom logger is in action." # Be sure to call_next. call_next context end def write(message) end end ``` You need to register your custom logger with `logger` config property. ```crystal require "kemal" Kemal.config.logger = MyCustomLogger.new ``` ## Kemal Middlewares The Kemal organization has a variety of useful middleware: - [kemal-basic-auth](https://github.com/kemalcr/kemal-basic-auth): Add HTTP Basic Authorization to your Kemal application. - [kemal-hmac](https://github.com/kemalcr/kemal-hmac): HMAC middleware for Kemal # Caching For expensive HTML pages, catalog-style APIs, or read-heavy routes, you can cache HTTP responses with [kemal-cache](https://github.com/kemalcr/kemal-cache). It is Kemal-native middleware with in-memory or Redis storage, automatic `ETag` / `Last-Modified`, and `304 Not Modified` for conditional requests. Responses include `X-Kemal-Cache: HIT` or `X-Kemal-Cache: MISS` so you can see behavior during development. ## Installation Add `kemal-cache` beside `kemal` in `shard.yml`: ```yaml dependencies: kemal: github: kemalcr/kemal kemal-cache: github: kemalcr/kemal-cache ``` Then run `shards install`. ## Examples Default setup uses the in-memory store, caches successful `GET` responses for ten minutes, and skips authenticated or cookie-bearing requests. Register the handler before your routes: ```crystal require "kemal" require "kemal-cache" use Kemal::Cache::Handler.new get "/articles" do "Expensive response" end Kemal.run ``` Tune TTL and other options with `Kemal::Cache::Config`: ```crystal config = Kemal::Cache::Config.new(expires_in: 2.minutes) use Kemal::Cache::Handler.new(config) get "/api/products" do ProductSerializer.render(ProductQuery.latest) end Kemal.run ``` For Redis, add the `redis` shard and `require "kemal-cache/redis"` — see below. ## Redis Add the Redis client to `shard.yml` (see the [redis shard](https://github.com/jgaskins/redis) for the current dependency line), then use `RedisStore` when several app processes or servers should share one cache: ```crystal require "kemal-cache/redis" store = Kemal::Cache::RedisStore.from_env("REDIS_URL", namespace: "my-app-cache") config = Kemal::Cache::Config.new(store: store) use Kemal::Cache::Handler.new(config) ``` Full options, invalidation, custom stores, and observability are documented in the [kemal-cache repository](https://github.com/kemalcr/kemal-cache). # File Upload Kemal provides easy access to uploaded files through `env.params.files`. When a file is uploaded via a form, it's automatically stored in a temporary location and accessible through the parameter name. ## Basic File Upload ```crystal post "/upload" do |env| file = env.params.files["image"].tempfile file_path = ::File.join [Kemal.config.public_folder, "uploads/", File.basename(file.path)] File.open(file_path, "w") do |f| IO.copy(file, f) end "Upload successful!" end ``` ## Advanced File Upload with Validation ```crystal post "/upload" do |env| unless env.params.files.has_key?("image") halt env, status_code: 400, response: "No file uploaded" end uploaded_file = env.params.files["image"] # Validate file size (max 5MB) max_size = 5 * 1024 * 1024 if uploaded_file.size > max_size halt env, status_code: 400, response: "File too large" end # Validate file type allowed_extensions = [".jpg", ".jpeg", ".png", ".gif"] file_extension = File.extname(uploaded_file.filename || "").downcase unless allowed_extensions.includes?(file_extension) halt env, status_code: 400, response: "Invalid file type" end # Generate unique filename unique_filename = "#{Time.utc.to_unix}_#{uploaded_file.filename}" file_path = ::File.join [Kemal.config.public_folder, "uploads/", unique_filename] # Save the file File.open(file_path, "w") do |f| IO.copy(uploaded_file.tempfile, f) end "File uploaded successfully as: #{unique_filename}" end ``` ## File Upload Properties - `filename`: Original filename - `tempfile`: Temporary file object - `size`: File size in bytes - `headers`: HTTP headers ## Multiple File Upload ```crystal post "/upload-multiple" do |env| uploaded_file_names = [] of String if env.params.files.has_key?("images[]") env.params.files["images[]"].each do |uploaded_file| # Validate and save each file max_size = 5 * 1024 * 1024 next if uploaded_file.size > max_size unique_filename = "#{Time.utc.to_unix}_#{Random.rand(1000)}_#{uploaded_file.filename}" file_path = ::File.join [Kemal.config.public_folder, "uploads/", unique_filename] File.open(file_path, "w") do |f| IO.copy(uploaded_file.tempfile, f) end uploaded_file_names << unique_filename end end "Successfully uploaded #{uploaded_file_names.size} files" end ``` ## Testing File Upload You can test single file uploads using `curl`: ```bash curl -F "image=@/path/to/your/file.png" http://localhost:3000/upload ``` For multiple file uploads: ```bash curl -F "images[]=@/path/to/file1.png" -F "images[]=@/path/to/file2.jpg" http://localhost:3000/upload-multiple ``` # Sessions Kemal supports Sessions with [kemal-session](https://github.com/kemalcr/kemal-session). ## User Login / Logout Example ```crystal require "kemal" require "kemal-session" # Session Configuration Kemal::Session.config.secret = "my-secret-key" # User login (create session) post "/login" do |env| username = env.params.body["username"]?.to_s # In a real app you would authenticate here env.session.string("username", username) env.session.bool("logged_in", true) "Welcome #{username}, you're now logged in." end # Protected route using session get "/profile" do |env| unless env.session.bool?("logged_in") env.response.status_code = 401 next "Please log in first" end username = env.session.string("username") "Hello #{username}!" end # User logout (destroy session) post "/logout" do |env| env.session.destroy "You have been logged out." end Kemal.run ``` `kemal-session` has a generic API to multiple storage engines. The default storage engine is `MemoryEngine` which stores the sessions in process memory. You should only use `MemoryEngine` for development and testing purposes. ## Accessing the CSRF token To access the CSRF token of the active session you can do the following in your form: ```erb "> ``` # WebSockets Using *WebSockets* with Kemal is super easy! ```crystal ws "/" do |socket| # Send welcome message socket.send "Hello from Kemal!" # Handle incoming messages socket.on_message do |message| socket.send "Echo: #{message}" end # Handle close socket.on_close do puts "Connection closed" end end ws "/route2" do |socket| # Another WebSocket endpoint end ``` `ws` yields a second parameter for accessing the HTTP context: ```crystal ws "/" do |socket, context| headers = context.request.headers socket.send headers["Content-Type"]? end ``` ## Accessing Dynamic URL Params ```crystal ws "/:id" do |socket, context| id = context.ws_route_lookup.params["id"] socket.send "Connected to room #{id}" end ``` # Testing You can test your Kemal application using [spec-kemal](https://github.com/kemalcr/spec-kemal). First add `spec-kemal` to your `shard.yml`: ```yaml name: your-kemal-app version: 0.1.0 dependencies: kemal: github: kemalcr/kemal development_dependencies: spec-kemal: github: kemalcr/spec-kemal ``` Install dependencies: ```bash shards install ``` Require it in your `spec/spec_helper.cr`: ```crystal require "spec-kemal" require "../src/your-kemal-app" ``` Create a spec file `spec/your-kemal-app_spec.cr`: ```crystal require "./spec_helper" describe "Your::Kemal::App" do it "renders /" do get "/" response.body.should eq "Hello World!" end end ``` Run the tests: ```bash KEMAL_ENV=test crystal spec ``` # Static Files Any files you add to the `public` directory will be served automatically by Kemal. ``` app/ src/ your_app.cr public/ js/ jquery.js your_app.js css/ your_app.css index.html ``` Example HTML: ```html ... ``` # Configuration Kemal provides a powerful configuration system through `Kemal.config`. ## Server Configuration ### Host and Port ```crystal Kemal.config.host_binding = "127.0.0.1" # Default: "0.0.0.0" Kemal.config.port = 8080 # Default: 3000 ``` Command line: ```bash ./your_app --bind 127.0.0.1 --port 8080 ``` ### Max Request Body Size ```crystal Kemal.config.max_request_body_size = 1024 * 1024 * 10 # 10 MB # Default: 8 MB ``` ## Static Files Configuration ### Public Folder ```crystal Kemal.config.public_folder = "./assets" # Default: "./public" ``` ### Serve Static Files ```crystal Kemal.config.serve_static = false # Default: true # With options Kemal.config.serve_static = {"gzip" => true, "dir_listing" => false} ``` ### Static Headers ```crystal static_headers do |response, filepath, filestat| if filepath =~ /\.html$/ response.headers.add("Access-Control-Allow-Origin", "*") end response.headers.add("Content-Size", filestat.size.to_s) end ``` ## Logging Configuration ### Enable/Disable Logging ```crystal Kemal.config.logging = false # Default: true ``` ### Custom Logger ```crystal class MyCustomLogger < Kemal::BaseLogHandler def call(context) puts "Custom logger" call_next context end def write(message) end end Kemal.config.logger = MyCustomLogger.new ``` ## SSL Configuration ```crystal Kemal.config.ssl = true Kemal.config.ssl_certificate_file = "/path/to/cert.pem" Kemal.config.ssl_key_file = "/path/to/key.pem" ``` Command line: ```bash ./your_app --ssl --ssl-cert-file cert.pem --ssl-key-file key.pem ``` ## Environment Configuration ```bash export KEMAL_ENV=production ``` Or in code: ```crystal Kemal.config.env = "production" ``` ## Error Handling ### Powered By Header ```crystal Kemal.config.powered_by_header = false # Disable Kemal.config.powered_by_header = "MyApp" # Custom value ``` ### Always Rescue ```crystal Kemal.config.always_rescue = false # Default: true ``` ## Handler Configuration ### Add Custom Handlers ```crystal Kemal.config.add_handler MyCustomHandler.new ``` ### Shutdown Timeout ```crystal Kemal.config.shutdown_timeout = 10.seconds # Default: nil ``` # CLI Command-line flags: | Short | Long | Description | |-------|------------------------|--------------------------------| | `-b` | `--bind HOST` | Host to bind (default: 0.0.0.0)| | `-p` | `--port PORT` | Port (default: 3000) | | `-s` | `--ssl` | Enables SSL | | | `--ssl-key-file FILE` | SSL key file | | | `--ssl-cert-file FILE` | SSL certificate file | # SSL Start your Kemal with SSL support: ```bash crystal build --release src/your_app.cr ./your_app --ssl --ssl-key-file your_key_file --ssl-cert-file your_cert_file ``` # Security Best practices for securing your Kemal application: resource limits, security headers (Helmet), rate limiting, and the Defense shard for throttling and blocking malicious requests. ## Resource Limits ```crystal require "kemal" # Maximum request body size (10 MB) Kemal.config.max_request_body_size = 10 * 1024 * 1024 # Hide powered by header Kemal.config.powered_by_header = false # Always rescue in production Kemal.config.always_rescue = true ``` ## Helmet [Helmet](https://github.com/EvanHahn/crystal-helmet) helps you secure your Kemal app by setting various HTTP security headers. It's a port of the Node.js Helmet module. Add the shard and register the handlers early in your handler chain: ```yaml # shard.yml dependencies: helmet: github: EvanHahn/crystal-helmet ``` ```crystal require "kemal" require "helmet" # Add Helmet handlers (order matters – add early) add_handler Helmet::DNSPrefetchControllerHandler.new add_handler Helmet::FrameGuardHandler.new add_handler Helmet::InternetExplorerNoOpenHandler.new add_handler Helmet::NoSniffHandler.new add_handler Helmet::StrictTransportSecurityHandler.new(7.day) add_handler Helmet::XSSFilterHandler.new get "/" do "Hello World" end Kemal.run ``` Each handler sets a specific header (e.g. `X-Frame-Options`, `X-Content-Type-Options`, `Strict-Transport-Security`). See the [Helmet documentation](https://evanhahn.github.io/crystal-helmet/) for options and customization. ## Defense [Defense](https://github.com/defense-cr/defense) is a Crystal HTTP handler for throttling, blocking and tracking malicious requests (inspired by Rack::Attack). Add the shard and register the handler early in your handler chain: ```yaml # shard.yml dependencies: defense: github: defense-cr/defense ``` ```crystal require "kemal" require "defense" # Store: Redis (production) or MemoryStore (development/tests) Defense.store = Defense::RedisStore.new(url: ENV["REDIS_URL"]? || "redis://localhost:6379/0") # Defense.store = Defense::MemoryStore.new add_handler Defense::Handler.new # Throttle: 10 requests per minute per IP Defense.throttle("requests per minute", limit: 10, period: 60) do |request| request.remote_address.to_s end # Blocklist: block /admin for non-trusted IPs Defense.blocklist("block admin") do |request| request.path.starts_with?("/admin/") end # Safelist: never throttle/block localhost Defense.safelist("localhost") do |request| request.remote_address.to_s == "127.0.0.1" end get "/" do "Hello World" end Kemal.run ``` Throttled and blocked responses are configurable via `Defense.throttled_response=` and `Defense.blocked_response=`. Defense also supports Fail2Ban and Allow2Ban for banning after repeated violations. See the [Defense README](https://github.com/defense-cr/defense) for full options. # Deployment ## Production Build ### Basic Release Build ```bash crystal build --release --no-debug src/your_app.cr ``` ### Static Linking ```bash crystal build --release --static --no-debug src/your_app.cr ``` ## Docker Deployment ### Multi-Stage Dockerfile ```dockerfile # Build stage FROM crystallang/crystal:1.11.2-alpine AS builder WORKDIR /app COPY shard.yml shard.lock ./ RUN shards install --production COPY . . RUN crystal build --release --static --no-debug src/your_app.cr -o bin/app # Runtime stage FROM alpine:latest WORKDIR /app RUN apk add --no-cache libgcc COPY --from=builder /app/bin/app . COPY --from=builder /app/public ./public EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=3s \ CMD wget --spider http://localhost:3000/health || exit 1 CMD ["./app"] ``` ### Docker Compose ```yaml version: '3.8' services: app: build: . ports: - "3000:3000" environment: KEMAL_ENV: production DATABASE_URL: postgres://postgres:password@db:5432/myapp REDIS_URL: redis://redis:6379 depends_on: - db - redis restart: unless-stopped db: image: postgres:15-alpine environment: POSTGRES_DB: myapp POSTGRES_PASSWORD: password volumes: - postgres_data:/var/lib/postgresql/data redis: image: redis:7-alpine volumes: - redis_data:/data volumes: postgres_data: redis_data: ``` ## Cloud Platforms ### Heroku Create `Procfile`: ``` web: ./your_app --port $PORT --bind 0.0.0.0 ``` Add buildpack: ```bash heroku buildpacks:set https://github.com/crystal-lang/heroku-buildpack-crystal ``` Deploy: ```bash git push heroku main ``` ### Fly.io Initialize: ```bash fly launch ``` Create `fly.toml`: ```toml app = "my-kemal-app" primary_region = "iad" [build] image = "your-registry/my-kemal-app:latest" [env] KEMAL_ENV = "production" [http_service] internal_port = 3000 force_https = true auto_stop_machines = true auto_start_machines = true min_machines_running = 1 [[services]] protocol = "tcp" internal_port = 3000 [[services.ports]] port = 80 handlers = ["http"] [[services.ports]] port = 443 handlers = ["tls", "http"] ``` Deploy: ```bash fly deploy ``` ## VPS and Bare Metal ### Systemd Service Create `/etc/systemd/system/kemal-app.service`: ```ini [Unit] Description=Kemal Application After=network.target postgresql.service [Service] Type=simple User=www-data Group=www-data WorkingDirectory=/opt/myapp ExecStart=/opt/myapp/your_app Restart=always RestartSec=10 Environment=KEMAL_ENV=production Environment=PORT=3000 Environment=HOST=127.0.0.1 EnvironmentFile=/opt/myapp/.env NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ProtectHome=true ReadWritePaths=/opt/myapp/log /opt/myapp/tmp LimitNOFILE=65535 LimitNPROC=4096 [Install] WantedBy=multi-user.target ``` Enable and start: ```bash sudo systemctl daemon-reload sudo systemctl enable kemal-app sudo systemctl start kemal-app sudo systemctl status kemal-app ``` ### Nginx Reverse Proxy Create `/etc/nginx/sites-available/myapp`: ```nginx upstream kemal { server 127.0.0.1:3000 max_fails=3 fail_timeout=30s; keepalive 32; } server { listen 80; server_name example.com; return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; server_name example.com; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; add_header Strict-Transport-Security "max-age=31536000" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; client_max_body_size 50M; location /static/ { alias /opt/myapp/public/; expires 30d; add_header Cache-Control "public, immutable"; } location /ws { proxy_pass http://kemal; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_read_timeout 86400; } location / { proxy_pass http://kemal; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; } location /health { proxy_pass http://kemal; access_log off; } } ``` Enable: ```bash sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx ``` ### SSL with Let's Encrypt ```bash sudo apt install certbot python3-certbot-nginx sudo certbot --nginx -d example.com ``` ## Best Practices ### Environment Configuration ```crystal require "kemal" Kemal.config.env = ENV["KEMAL_ENV"]? || "development" Kemal.config.port = ENV["PORT"]?.try(&.to_i) || 3000 Kemal.config.host_binding = ENV["HOST"]? || "0.0.0.0" DATABASE_URL = ENV["DATABASE_URL"]? || "postgres://localhost/myapp_dev" SECRET_KEY = ENV["SECRET_KEY_BASE"]? || raise "SECRET_KEY_BASE required" ``` ### Logging and Monitoring ```crystal # Health check endpoint get "/health" do |env| env.response.content_type = "application/json" db_healthy = begin DB.open(DATABASE_URL) { |db| db.query_one("SELECT 1", as: Int32) } true rescue false end status = db_healthy ? "healthy" : "unhealthy" env.response.status_code = status == "healthy" ? 200 : 503 { status: status, timestamp: Time.utc.to_rfc3339, checks: { database: db_healthy ? "up" : "down" } }.to_json end ``` ### Graceful Shutdown ```crystal require "kemal" # Configure graceful shutdown Kemal.config.shutdown_timeout = 10.seconds # Your routes... get "/" do "Hello World" end Kemal.run ``` ### Connection Pooling `crystal-db` provides a built-in connection pool. `DB.open` returns a `DB::Database` object that manages the pool—you don't need to configure it manually. Configure the pool via **query string parameters** in your connection URI. **Pool parameters** (see [Crystal Connection Pool docs](https://crystal-lang.org/reference/1.19/database/connection_pool.html)): | Parameter | Default | Description | |-----------------------|---------|------------------------------------------------| | `initial_pool_size` | 1 | Connections created when opening the database | | `max_pool_size` | 0 | Maximum connections (0 = unlimited) | | `max_idle_pool_size` | 1 | Max idle connections before closing excess | | `checkout_timeout` | 5.0 | Seconds to wait for an available connection | | `retry_attempts` | 1 | Retries on connection loss | | `retry_delay` | 1.0 | Seconds between retries | ```crystal require "db" require "pg" # Configure pool via URI query parameters # postgres://user:pass@host/db?initial_pool_size=5&max_pool_size=25&checkout_timeout=5&retry_attempts=3 DB = DB.open(ENV["DATABASE_URL"]? || "postgres://localhost/myapp?initial_pool_size=5&max_pool_size=25&max_idle_pool_size=10&checkout_timeout=5&retry_attempts=3&retry_delay=1") # Use in routes - db.query, db.exec, etc. automatically use the pool get "/users" do |env| users = DB.query_all("SELECT * FROM users", as: User) users.to_json end ``` When using `db.query`, `db.exec`, `db.scalar`, etc., the pool automatically checks out a connection, runs the statement, and returns it to the pool. If the connection is lost, it retries according to `retry_attempts` and `retry_delay`. ## Cross-compilation ```bash # Basic cross-compilation crystal build --cross-compile --target x86_64-unknown-linux-gnu src/your_app.cr # Docker-based docker run --rm -v $(pwd):/app -w /app crystallang/crystal:latest \ crystal build --release --static --no-debug src/your_app.cr -o bin/app-linux ``` # End of Kemal documentation