Sign in Sign up

Clone of github.com/kemalcr/kemal

Public mirror on neosource. Source code and history come from the original repository under its upstream license.

Original on GitHub
72 branches · 68 tags
README.md

Kemal

Kemal - Fast, Effective, Simple Web Framework for Crystal

#

Build web applications and APIs with minimal code. 3.8k+ ⭐, 5M+ downloads since 2015.

Stars CI Release Downloads Crystal License Built with Crystal


Quick Start

#
  1. Create a new project

    crystal init app my-app
    cd my-app
    
  2. Add Kemal to shard.yml

    dependencies:
      kemal:
        github: kemalcr/kemal
    
  3. Write your app - replace src/my-app.cr with:

    require "kemal"
    
    get "/" do
      "Hello World!"
    end
    
    get "/api" do |env|
      env.response.content_type = "application/json"
      {status: "ok"}.to_json
    end
    
    Kemal.run
    
  4. Install dependencies and run

    shards install
    crystal run src/my-app.cr
    

Visit http://localhost:3000 - done in under a minute. 🚀


Why Kemal?

#
Problem Solution
"I want C-level performance with Ruby-like syntax" Crystal + Kemal - fast by default
"I need WebSocket support out of the box" Built-in, no extra gems
"Building a JSON API" Native JSON handling, minimal boilerplate
"I want a framework that stays out of my way" No forced ORM, no magic, just Crystal

Key Features

#
  • 🚀 High-performance by default - Built on Crystal with a thin abstraction layer
  • 🌐 Full REST & HTTP support - All HTTP verbs with a straightforward routing DSL
  • 🔌 WebSocket & real-time - First-class WebSocket support
  • 📦 JSON-first APIs - Native JSON handling
  • 🗄️ Static assets - Serve files efficiently from the same application
  • 📝 Template engine - Built-in ECR template engine
  • 🔒 Composable middleware - Logging, auth, rate limiting, metrics
  • 🎯 Ergonomic request/response - Simple params, headers, cookies via context
  • 🍪 Session management - Production-ready via kemal-session

Performance

#

Kemal is built on Crystal. It compiles to native code and starts instantly.

Test Results
JSON serialization (100 conn) ~50,000 req/sec
Hello World (100 conn) ~85,000 req/sec
Static file serving ~40,000 req/sec
Memory per request ~0.5 KB
Binary size ~2 MB (with dependencies)

No JVM, no Node, no Ruby VM. Just a native binary.


How Kemal compares

#
Feature Kemal Sinatra Flask Express
Performance (req/sec) ~85K ~5K ~3K ~15K
WebSocket built-in ✅ - - -
Single binary deploy ✅ - - -
JSON handling Native Gem Extension Native
Type safety ✅ - - -
Concurrency Fibers Threads Threads Async

Philosophy

#

Kemal aims to be a simple, fast and reliable foundation for building production-grade web applications and APIs in Crystal.

  • Simple core, powerful building blocks - The core is intentionally simple. Most power comes from Crystal and middleware, not hidden magic.
  • Performance as a baseline - Crystal's native speed means high performance is the default. Kemal keeps abstractions thin.
  • Minimal assumptions, maximum flexibility - No forced ORM, template engine, or project layout. Choose your own tools.
  • Batteries within reason - Ships with essentials (routing, middleware, templates, static files) while keeping advanced concerns in separate shards.

Learning Resources

#

FAQ

#

Is Kemal production ready?

Yes. Kemal has been used in production since 2015 with 5M+ downloads.

Does Kemal support WebSocket?

Yes, built-in. No extra dependencies needed.

Can I build a REST API with Kemal?

Yes. JSON handling is built-in. Return hashes or JSON directly from routes.

Does Kemal support the HTTP QUERY method?

Yes. QUERY (RFC 10008) is a safe, idempotent method that carries the query in the request body instead of the URL:

query "/search" do |env|
  q = env.params.json["q"]? # or env.params.body for form-encoded queries
  search_products(q).to_json
end

before_query / after_query filters and Kemal::Router#query work like every other verb. A QUERY request that has a body but no Content-Type header is rejected with 400 per the RFC.

What happens when a path exists but the request method doesn't match?

Kemal answers 405 Method Not Allowed with an Allow header listing the methods that path does accept, per RFC 9110 §15.5.6. A path that isn't routed at all is still a 404.

get "/posts" do
  "posts"
end

# POST /posts  -> 405, Allow: GET, HEAD
# GET  /nope   -> 404

HEAD is included in Allow wherever a GET route exists, since Kemal serves HEAD from the GET route. Register error 405 to customize the body — the Allow header is set before your handler runs, so the handler owns the body but cannot drop the header the RFC requires.

error 405 do |env|
  env.response.content_type = "application/json"
  {error: "Method not allowed", allow: env.response.headers["Allow"]}.to_json
end

A ws route counts as GET, because the WebSocket handshake is a GET request. It is listed without HEAD, and a plain GET on such a path — a handshake missing its Upgrade header — stays a 404 rather than being answered with a nonsensical Allow: GET.

ws "/chat" do |socket, env|
  socket.send("hi")
end

post "/chat" do
  "post"
end

# PUT  /chat -> 405, Allow: GET, POST
# GET  /chat -> 404 (no `Upgrade` header, so not a handshake)

NOTE: Kemal::InitHandler presets Content-Type: text/html on every response, so an error handler returning anything else has to set the content type itself.

NOTE: Put authentication in before_all or in middleware rather than in a path-scoped filter like before_get "/admin/*". Path-scoped filters do not run when no route matches, so a wrong-method request answers 405 with Allow — confirming the path exists — without ever reaching the guard.

What does a user see when a route raises?

In the development environment, a page with the exception, its backtrace and source, the response headers and cookies. In every other environment — production, staging, test, or a value Kemal has never heard of — a static "Kemal has encountered an error" page that says nothing about the failure. The environment comes from KEMAL_ENV and defaults to development.

# Show the development page in another environment, or never show it.
Kemal.config.show_exceptions = true
Kemal.config.show_exceptions = false

Register error 500 to render your own page instead; the setting only decides between Kemal's two built-in ones.

Can I use before_all in my specs?

Yes. Kemal's before_all and after_all share their names with the describe-level hooks of Crystal's spec library, and as top-level definitions they take precedence. Called inside a describe block they act as the spec hooks — the block runs once around the group's examples — so a spec file reads the same with or without Kemal loaded. Called anywhere else, including inside an example, they register Kemal's filter.

describe "Users" do
  before_all { seed_users }   # the spec hook: once, before the examples

  it "lists them" do
    before_all { |env| env.set "user", "bob" }   # Kemal's filter, for this example's requests
    get "/users" { |env| env.get("user").to_s }
    call_request_on_app(HTTP::Request.new("GET", "/users")).body.should eq("bob")
  end
end

Does Kemal work with any ORM?

Yes. You can use any Crystal ORM or database library. No forced dependencies.

How is Kemal different from Sinatra / Flask / Express?

Kemal compiles to a native binary with Crystal. You get C-like performance, type safety, and single-binary deployment. Sinatra and Flask are interpreted. Express runs on Node's event loop.


Sponsors

#

If Kemal helps you or your company, consider sponsoring. Your support helps me maintain Kemal and build the Crystal ecosystem full time.

Sponsor

Corporate

#

Marsus
Marsus

Crystal Champions

#

Fatih Kadir Akın
Fatih Kadir Akın

Backers

#
Aurélien Delogu
Aurélien Delogu
David YOTEAU
David YOTEAU
Mesut Vatansever
Mesut Vatansever

Supporters

#

Marvin Ahlgrimm hahwul Jeremy Woertink laktosterror Luis Lavena Nick Brandaleone Tarık Çayır


Contributing

#

We love contributions! Please read our Contributing Guide.

Security

#

To report a security vulnerability, please see our Security Policy.


Acknowledgments

#

Special thanks to Manas for their work on Frank.


License

#

MIT