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


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.


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