Fast, Effective, Simple Web Framework



Lightning Fast, Super Simple web framework.

Build Status Join the chat at

Super Simple ⚡️

require "kemal"

# Matches GET "http://host:port/"
get "/" do
  "Hello World!"

# Creates a WebSocket handler.
# Matches "ws://host:port/socket"
ws "/socket" do |socket|
  socket.send "Hello from Kemal!"

Start your application!

crystal src/

Go to http://localhost:3000

Check documentation or samples for more.


Add this to your application's shard.yml:

    github: kemalcr/kemal

See also Getting Started.


  • Support all REST verbs
  • Websocket support
  • Request/Response context, easy parameter handling
  • Middleware support
  • Built-in JSON support
  • Built-in static file serving
  • Built-in view templating via Kilt


You can read the documentation at the official site


Thanks to Manas for their awesome work on Frank.

0.26.1 (01-12-2019)

  • Fix process request when a response already closed #550. Thanks @mamantoha 🙏
  • Switch to new Ameba repository #549. Thanks @mamantoha 🙏
  • Check for KEMAL_ENV variable already in Config#initialize#552. Thanks @Sija 🙏
  • Cleanup Ameba warnings #551. Thanks @Sija 🙏
  • Flush io buffer after each write to log #554. Thanks @mang 🙏

0.26.0 (05-08-2019)

  • Crystal 0.30.0 support 🎉 #548 and #544. Thanks @bcardiff and @straight-shoota 🙏
  • Add support for serving files greater than 2^31 bytes #546. Thanks @omarroth 🙏
  • Properly measure request time using Time.monotonic #527. Thanks @spinscale 🙏

0.25.2 (08-02-2019)

  • Add option to config to parse or not command line parameters #483. Thanks @diegogub 🙏

  • Allow to set filename for send_file #512. Thanks @mamantoha 🙏

send_file env, "./asset/image.jpeg", filename: "image.jpg"
  • Set status_code before response #513. Thanks @mamantohoa 🙏

  • Use Crystal MIME registry. #516 Thanks @Sija 🙏

0.25.1 (06-10-2018)

0.25.0 (05-10-2018)

  • Crystal 0.27.0 support.
  • [breaking change] Added back env.params.files.

Here's a fully working sample for reading a image file upload image1 and saving it under public/uploads.

post "/upload" do |env|
  file = env.params.files["image1"].tempfile
  file_path = ::File.join [Kemal.config.public_folder, "uploads/", File.basename(file.path)], "w") do |f|
    IO.copy(file, f)
  "Upload ok"

To test

curl -F "image1=@/Users/serdar/Downloads/kemal.png" http://localhost:3000/upload

0.24.0 (14-08-2018)

  • [breaking change] Removed env.params.files. You can use Crystal's built-in HTTP::FormData.parse instead
post "/upload" do |env|
  HTTP::FormData.parse(env.request) do |upload|
    filename = file.filename

    if !filename.is_a?(String)
      "No filename included in upload"
      file_path = ::File.join [Kemal.config.public_folder, "uploads/", filename], "w") do |f|
      IO.copy(file.tmpfile, f)
    "Upload OK"
  • [breaking change] From now on to access dynamic url params in a WebSocket route you have to use:
ws "/:id" do |socket, context|
  id = context.ws_route_lookup.params["id"]
  • [breaking change] Removed _method magic param.

  • Added new exception page #466. Thanks @mamantoha 🙏

  • Support custom port binding. Thanks @straight-shoota 🙏 do |config|
  server = config.server.not_nil!
  server.bind_tcp "", 3000, reuse_port: true
  server.bind_tcp "", 3001, reuse_port: true

0.23.0 (17-06-2018)

  • Crystal 0.25.0 support 🎉
  • Add Kemal::Context.get? to safely access context storage 😎
  • [Security] Don't serve 404 image dynamically 👍
  • Disable X-Powered-By header #449. Thanks @Blacksmoke16 🙏

0.22.0 (29-12-2017)

  • Crystal 0.24.1 support 🎉
  • Only return string from route.#408 thanks @crisward 🙏
  • Don't crash on empty path when compiled in --release. #407 thanks @crisward 🙏
  • Rename Kemal::CommonLogHandler to Kemal::LogHandler and Kemal::CommonExceptionHandler to Kemal::ExceptionHandler.
  • Allow videos to be opened with correct mime type. #406 thanks @crisward 🙏
  • Add webm mime type.#413 thanks @reindeer-cafe 🙏

0.21.0 (05-09-2017)

  • Dynamically insert handlers 💪 Fixes #376.
  • Add context to WebSocket. This allows one to use HTTP::Server::Context in ws declarations 😍 Fixes #349.
ws "/:room_name" do |socket, env|
  • Add support for customizing the headers of built-in Kemal::StaticFileHandler 🔨 Useful for supporting CORS for single page applications 👏
static_headers do |response, filepath, filestat|
  if filepath =~ /\.html$/
      response.headers.add("Access-Control-Allow-Origin", "*")
    response.headers.add("Content-Size", filestat.size.to_s)
  • Allow %w in Handler macros #385. Thanks @will 🙏

  • Security: X-Content-Type-Options: nosniff for static files. Fixes #379. Thanks @crisward 🙏

  • Performance: Remove tempfile management to OS. This brings %10 - 15 performance boost to Kemal 🚀

0.20.0 (01-07-2017)

  • Crystal 0.23.0 support! As always, Kemal is compatible with the latest major release of Crystal 💎
  • Great news everyone 🎉 All handlers are now completely customizable!. Use the default Kemal handlers or go wild, it's all up to you ⛏
# Don't forget to add `Kemal::RouteHandler::INSTANCE` or your routes won't work!
Kemal.config.handlers = [,, Kemal::RouteHandler::INSTANCE]

You can also insert a handler into a specific position.

# This adds MyCustomHandler instance to 1 position. Be aware that the index starts from 0.
add_handler, 1
  • Updated Kilt to v0.4.0.
  • Make Route a Struct. This improves the performance of route declarations.

0.19.0 (09-05-2017)

  • Return no body for head route fixes #323. (thanks @crisward)
  • Update radix to 0.3.8. (thanks @waghanza)
  • User defined context store types. (thanks @neovitange)
class User
   property name

  • Prevent `send_file returning filesize. (thanks @crisward)
  • Dont call setup in config#add_filter_handler fixes #338.

0.18.3 (07-03-2017)

  • Remove Gzip::Header monkey patch since it's fixed in Crystal 0.21.1.

0.18.2 (24-02-2017)

0.18.1 (21-02-2017)

  • Crystal 0.21.0 support
  • Drop dependency. multipart support is now built-into Crystal <3
  • Since Crystal 0.21.0 comes built-in with multipart there are some improvements and deprecations.

meta has been removed from FileUpload and it has the following properties

  • tmpfile: This is temporary file for file upload. Useful for saving the upload file.
  • filename: File name of the file upload. (logo.png, e.g)
  • headers: Headers for the file upload.
  • creation_time: Creation time of the file upload.
  • modification_time: Last Modification time of the file upload.
  • read_time: Read time of the file upload.
  • size: Size of the file upload.

0.18.0 (11-02-2017)

  • Simpler file upload. File uploads can now be access from HTTP::Server::Context like env.params.files["filename"].

env.params.files["filename"] has 5 methods

  • tmpfile: This is temporary file for file upload. Useful for saving the upload file.
  • tmpfile_path: File path of tmpfile.
  • filename: File name of the file upload. (logo.png, e.g)
  • meta: Meta information for the file upload.
  • headers: Headers for the file upload.

Here's a fully working sample for reading a image file upload image1 and saving it under public/uploads.

post "/upload" do |env|
file = env.params.files["image1"].tmpfile
file_path = ::File.join [Kemal.config.public_folder, "uploads/", file.filename], "w") do |f|
  IO.copy(file, f)
"Upload ok"

To test

curl -F "image1=@/Users/serdar/Downloads/kemal.png" http://localhost:3000/upload

0.17.5 (09-01-2017)

  • Update to 0.1.2. Fixes #285 related to

0.17.4 (24-12-2016)

0.17.3 (03-12-2016)

0.17.2 (25-11-2016)

  • Use body.gets_to_end for parse_json. Fixes #260.
  • Update Radix to 0.3.5 and lock pessimistically. (thanks @luislavena)

0.17.1 (24-11-2016)

  • Treat HTTP::Request body as an IO. Fixes #257

0.17.0 (23-11-2016)

  • Reimplemented Request middleware / filter routing.

Now all requests will first go through the Middleware stack then Filters (before_*) and will finally reach the matching route.

Which is illustrated as,

Request -> Middleware -> Filter -> Route
  • Rename return_with as halt.
  • Route declaration must start with /. Fixes #242
  • Set default exception Content-Type to text/html. Fixes #202
  • Add only and exclude paths for Kemal::Handler. This change requires that all handlers must inherit from Kemal::Handler.

For example this handler will only work on / path. By default the HTTP method is GET.

class OnlyHandler < Kemal::Handler
  only ["/"]

  def call(env)
    return call_next(env) unless only_match?(env)
    puts "If the path is / i will be doing some processing here."

The handlers using exclude will work on the paths that isn't specified. For example this handler will work on any routes other than /.

class ExcludeHandler < Kemal::Handler
  exclude ["/"]

  def call(env)
    return call_next(env) unless only_match?(env)
    puts "If the path is NOT / i will be doing some processing here."
  • Close response on halt. (thanks @samueleaton).
  • Update Radix to v0.3.4.
  • error handler now also yields error. For example you can get the error mesasage like
  error 500 do |env, err|
  • Update to v0.1.1

0.16.1 (12-10-2016)

  • Improved Multipart support with more info on parsed files. parse_multipart(env) now yields an UploadFile object which has the following properties field,data,meta,`headers.
post "/upload" do |env|
  parse_multipart(env) do |f|
    image1 = if f.field == "image1"
    image2 = if f.field == "image2"
    puts f.meta
    puts f.headers
    "Upload complete"


  • Multipart support <3 (thanks @RX14). Now you can handle file uploads.
post "/upload" do |env|
  parse_multipart(env) do |field, data|
    image1 = data if field == "image1"
    image2 = data if field == "image2"
    "Upload complete"
  • Make session configurable. Now you can specify session name and expire time wit
Kemal.config.session["name"] = "your_app"
Kemal.config.session["expire_time"] = 48.hours
  • Session now supports more types. (String, Int32, Float64, Bool)
  • Add gzip helper to enable / disable gzip compression on responses.
  • Static file caching with etag and gzip (thanks @crisward)
  • now accepts port to listen.

0.15.1 (05-09-2016)

  • Don't forget to call_next on NullLogHandler

0.15.0 (03-09-2016)

  • Add context store
  • KEMAL_ENV respects to Kemal.config.env and needs to be explicitly set.
  • Kemal::InitHandler is introduced. Adds initial configuration, headers like X-Powered-By.
  • Add send_file to helpers.
  • Add mime types.
  • Fix parsing JSON params when "charset" is present in "Content-Type" header.
  • Use http-only cookie for session
  • Inject STDOUT by default in CommonLogHandler
Github statistic:
  • 2739
  • 525
  • 140
  • 85
  • 15
  • 1 day ago


MIT License