diff --git a/.ameba.yml b/.ameba.yml
deleted file mode 100644
index d7b2b9a..0000000
--- a/.ameba.yml
+++ /dev/null
@@ -1,24 +0,0 @@
-# This configuration file was generated by `ameba --gen-config`
-# on 2023-01-30 12:35:15 UTC using Ameba version 1.4.0.
-# The point is for the user to remove these configuration records
-# one by one as the reported problems are removed from the code base.
-
-# Problems found: 2
-# Run `ameba --only Lint/UselessAssign` for details
-Lint/UselessAssign:
- Description: Disallows useless variable assignments
- Excluded:
- - spec/view_spec.cr
- Enabled: true
- Severity: Warning
-
-# Problems found: 6
-# Run `ameba --only Lint/NotNil` for details
-Lint/NotNil:
- Description: Identifies usage of `not_nil!` calls
- Excluded:
- - src/kemal/param_parser.cr
- - src/kemal/static_file_handler.cr
- - src/kemal/config.cr
- Enabled: true
- Severity: Warning
diff --git a/.github/workflows/ameba.yml b/.github/workflows/ameba.yml
new file mode 100644
index 0000000..e321d6a
--- /dev/null
+++ b/.github/workflows/ameba.yml
@@ -0,0 +1,19 @@
+name: Ameba
+
+on:
+ push:
+ pull_request:
+
+permissions:
+ contents: read
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Download source
+ uses: actions/checkout@v6
+
+ - name: Run Ameba Linter
+ uses: crystal-ameba/github-action@master
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f473bda..8e65750 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -30,47 +30,3 @@ jobs:
- name: Run specs
run: |
crystal spec
-
- format:
- strategy:
- fail-fast: false
- matrix:
- os: [ubuntu-latest]
- crystal: [latest, nightly]
- runs-on: ${{ matrix.os }}
-
- steps:
- - name: Install Crystal
- uses: crystal-lang/install-crystal@v1
- with:
- crystal: ${{ matrix.crystal }}
-
- - name: Download source
- uses: actions/checkout@v4
-
- - name: Check formatting
- run: crystal tool format --check
-
- ameba:
- strategy:
- fail-fast: false
- matrix:
- os: [ubuntu-latest]
- crystal: [latest]
- runs-on: ${{ matrix.os }}
-
- steps:
- - name: Install Crystal
- uses: crystal-lang/install-crystal@v1
- with:
- crystal: ${{ matrix.crystal }}
-
- - name: Download source
- uses: actions/checkout@v4
-
- - name: Install dependencies
- run: shards install
-
- - name: Run ameba linter
- run: bin/ameba
-
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 130192c..2382024 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,156 @@
+# 1.10.1 (24-03-2026)
+
+- Add `shutdown_timeout` configuration for graceful shutdown: after `Kemal.stop`, Kemal can wait before exit so in-flight work can finish [#745](https://github.com/kemalcr/kemal/pull/745). Thanks @sdogruyol :pray:
+
+```crystal
+Kemal.config.shutdown_timeout = 10.seconds
+```
+
+# 1.10.0 (03-03-2026)
+
+- Add modular `Kemal::Router` with namespaced routing, scoped filters, WebSocket support and flexible mounting while keeping the existing DSL fully compatible [#731](https://github.com/kemalcr/kemal/pull/731). Thanks @sdogruyol :pray:
+
+```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
+```
+
+- Add `use` keyword for registering global and path-specific middleware, including support for arrays and insertion at a specific position in the handler chain [#734](https://github.com/kemalcr/kemal/pull/734). Thanks @sdogruyol :pray:
+
+```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
+```
+
+- Enhance response helpers to provide chainable JSON/HTML/text/XML helpers, `HTTP::Status` support and the ability to halt execution from a chained response for concise API error handling [#733](https://github.com/kemalcr/kemal/pull/733), [#735](https://github.com/kemalcr/kemal/pull/735), [#736](https://github.com/kemalcr/kemal/pull/736). Thanks @sdogruyol and @mamantoha :pray:
+
+```crystal
+require "kemal"
+
+get "/users" do |env|
+ # Default JSON response
+ env.json({users: ["alice", "bob"]})
+end
+
+post "/users" do |env|
+ # Symbol-based HTTP::Status and chained JSON
+ env.status(:created).json({id: 1, created: true})
+end
+
+get "/admin" do |env|
+ # Halt immediately with HTML response
+ halt env.status(403).html("
Forbidden
")
+end
+
+get "/api/users" do |env|
+ # Custom content type (JSON:API)
+ env.json({data: ["alice", "bob"]}, content_type: "application/vnd.api+json")
+end
+
+Kemal.run
+```
+
+- Ensure global wildcard filters always execute while keeping namespace filters isolated to their routes [#737](https://github.com/kemalcr/kemal/pull/737). Thanks @mamantoha :pray:
+- Fix CLI SSL validation and expand CLI option parsing specs [#738](https://github.com/kemalcr/kemal/pull/738). Thanks @sdogruyol :pray:
+- Make route LRU cache concurrency-safe with Mutex [#739](https://github.com/kemalcr/kemal/pull/739). Thanks @sdogruyol :pray:
+- Add `raw_body` to ParamParser for multi-handler body access (e.g. kemal-session) [#740](https://github.com/kemalcr/kemal/pull/740). Thanks @sdogruyol :pray:
+
+```crystal
+post "/" do |env|
+ raw = env.params.raw_body # raw body, multiple handlers can call it
+ env.params.body["name"] # parsed body
+end
+```
+
+- Fix OverrideMethodHandler route cache bug when using `_method` override [#741](https://github.com/kemalcr/kemal/pull/741), [#742](https://github.com/kemalcr/kemal/pull/742). Thanks @skojin and @sdogruyol :pray:
+
+# 1.9.0 (28-01-2026)
+
+- Crystal 1.19.0 support :tada:
+- ***(SECURITY)*** Limit maximum request body size to avoid DoS attacks [#730](https://github.com/kemalcr/kemal/pull/730). Thanks @sdogruyol :pray:
+- Optimize JSON parameter parsing by directly using the request body IO. Thanks @sdogruyol :pray:
+
+# 1.8.0 (07-11-2025)
+
+- Enhance HEAD request handling by caching GET route lookups and optimize path construction using string interpolation for improved performance [#728](https://github.com/kemalcr/kemal/pull/728). Thanks @sdogruyol :pray:
+- Improve error messages [#726](https://github.com/kemalcr/kemal/pull/726). Thanks @sdogruyol :pray:
+- Optimize route and websocket lookups by caching results to reduce redundant processing in the HTTP server context [#725](https://github.com/kemalcr/kemal/pull/725). Thanks @sdogruyol :pray:
+- Replace full-flush Route cache with LRU and add a configurable max cache size [#724](https://github.com/kemalcr/kemal/pull/724). Thanks @sdogruyol :pray:
+
+# 1.7.3 (02-10-2025)
+
+- Refactor [#719](https://github.com/kemalcr/kemal/pull/719). Thanks @sdogruyol :pray:
+- Improve Kemal test suite. Thanks @sdogruyol :pray:
+
+# 1.7.2 (04-08-2025)
+
+- Move Kemal::Handler logic into separate module [#717](https://github.com/kemalcr/kemal/pull/717). Thanks @syeopite :pray:
+- Refactor server binding logic to avoid binding in test environment [#719](https://github.com/kemalcr/kemal/pull/719). Thanks @sdogruyol :pray:
+
+# 1.7.1 (14-04-2025)
+
+- Improve `StaticFileHandler` to align with latest Crystal implementation [#711](https://github.com/kemalcr/kemal/pull/711). Thanks @sdogruyol :pray:
+
+# 1.7.0 (14-04-2025)
+
+- ***(SECURITY)*** Fix a Path Traversal Security issue in `StaticFileHandler`. [See](https://packetstorm.news/files/id/190294/) for more details. Thanks a lot @ahmetumitbayram :pray:
+- Crystal 1.16.0 support :tada:
+- Add ability to add handlers for raised exceptions [#688](https://github.com/kemalcr/kemal/pull/688). Thanks @syeopite :pray:
+
+```crystal
+require "kemal"
+
+class NewException < Exception
+end
+
+get "/" do | env |
+ raise NewException.new()
+end
+
+error NewException do | env |
+ "An error occured!"
+end
+
+Kemal.run
+```
+
+- Add `all_files` method to `params` to support multiple file uploads in names ending with `[]` [#701](https://github.com/kemalcr/kemal/pull/701). Thanks @sdogruyol :pray:
+
+```crystal
+images = env.params.all_files["images[]"]?
+```
+
+- Embrace Crystal standard Log for logging [#705](https://github.com/kemalcr/kemal/pull/705). Thanks @hugopl :pray:
+- Cleanup temporary files for file uploads [#707](https://github.com/kemalcr/kemal/pull/707). Thanks @sdogruyol :pray:
+- Implement multiple partial ranges [#708](https://github.com/kemalcr/kemal/pull/708). Thanks @sdogruyol :pray:
+
# 1.6.0 (12-10-2024)
- Crystal 1.14.0 support :tada:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..356782c
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,59 @@
+# Contributing to Kemal
+
+Thank you for your interest in contributing to Kemal! We love pull requests from everyone.
+
+## Getting Started
+
+1. **Fork** the repository on GitHub.
+2. **Clone** your fork locally:
+ ```bash
+ git clone https://github.com/YOUR_USERNAME/kemal.git
+ cd kemal
+ ```
+3. **Install dependencies**:
+ ```bash
+ shards install
+ ```
+
+## Running Tests
+
+Before submitting a pull request, please ensure that all tests pass.
+
+```bash
+crystal spec
+```
+
+## Code Style
+
+Kemal follows the standard Crystal code style. Please ensure your code is formatted correctly before committing.
+
+```bash
+crystal tool format
+```
+
+## Submitting a Pull Request
+
+1. Create a new branch for your feature or bug fix:
+ ```bash
+ git checkout -b my-new-feature
+ ```
+2. Commit your changes with descriptive commit messages.
+3. Push your branch to your fork:
+ ```bash
+ git push origin my-new-feature
+ ```
+4. Open a **Pull Request** on the main Kemal repository.
+5. Describe your changes and link to any relevant issues.
+
+## Reporting Bugs
+
+If you find a bug, please open an issue on GitHub with:
+- A clear title and description.
+- Steps to reproduce the issue.
+- The version of Kemal and Crystal you are using.
+
+## Feature Requests
+
+We welcome new ideas! Please open an issue to discuss your feature request before implementing it.
+
+Thank you for contributing to Kemal! π
diff --git a/README.md b/README.md
index aba67b4..de923a3 100644
--- a/README.md
+++ b/README.md
@@ -2,45 +2,32 @@
# Kemal
-Lightning Fast, Super Simple web framework.
+Kemal is the Fast, Effective, Simple Web Framework for Crystal. It's perfect for building Web Applications and APIs with minimal code.
**THIS IS A FORK OF KEMAL. DIRECT TO FORK.MD FOR SPECIFICS ON THIS FORK.**
[](https://github.com/kemalcr/kemal/actions/workflows/ci.yml)
-[](https://gitter.im/sdogruyol/kemal?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
-# Super Simple β‘οΈ
+## Why Kemal?
-```ruby
-require "kemal"
+- π **Lightning Fast**: Built on Crystal, known for C-like performance
+- π‘ **Super Simple**: Minimal code needed to get started
+- π **Feature Rich**: Everything you need for modern web development
+- π§ **Flexible**: Easy to extend with middleware support
-# Matches GET "http://host:port/"
-get "/" do
- "Hello World!"
-end
+## Quick Start
-# Creates a WebSocket handler.
-# Matches "ws://host:port/socket"
-ws "/socket" do |socket|
- socket.send "Hello from Kemal!"
-end
+1. First, make sure you have [Crystal installed](https://crystal-lang.org/install/).
-Kemal.run
+2. Create a new Crystal application and step into it:
+
+```bash
+crystal init app my-kemal-app
+cd my-kemal-app
```
-Start your application!
-
-```
-crystal src/kemal_sample.cr
-```
-
-Go to _http://localhost:3000_
-
-Check [documentation](http://kemalcr.com) or [samples](https://github.com/kemalcr/kemal/tree/master/samples) for more.
-
-# Installation
-
-Add this to your application's `shard.yml`:
+3. Add Kemal to your app's `shard.yml`:
+>>>>>>> upstream/master
```yaml
dependencies:
@@ -48,22 +35,78 @@ dependencies:
github: kemalcr/kemal
```
-See also [Getting Started](http://kemalcr.com/guide/).
+4. Replace the contents of `src/my_kemal_app.cr` with your first Kemal app:
-# Features
+```crystal
+require "kemal"
-- 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 [ECR](https://crystal-lang.org/api/ECR.html)
+# Basic route - responds to GET "http://localhost:3000/"
+get "/" do
+ "Hello World!"
+end
-# Documentation
+# JSON API example
+get "/api/status" do |env|
+ env.response.content_type = "application/json"
+ {"status": "ok"}.to_json
+end
-You can read the documentation at the official site [kemalcr.com](http://kemalcr.com)
+# WebSocket support
+ws "/chat" do |socket|
+ socket.send "Hello from Kemal WebSocket!"
+end
-## Thanks
+Kemal.run
+```
-Thanks to Manas for their awesome work on [Frank](https://github.com/manastech/frank).
+5. Install dependencies and run your application:
+
+```bash
+shards install
+crystal run src/my_kemal_app.cr
+```
+
+6. Visit [http://localhost:3000](http://localhost:3000) - That's it! π
+
+## Key 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](https://github.com/kemalcr/kemal-session), suitable for production apps.
+
+## 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 and easy to reason about. Most power comes from Crystal itself and from middleware, not from hidden magic.
+- **Performance as a baseline, not a feature**: Crystal's native speed means high performance is the default. Kemal keeps abstractions thin so you stay close to the metal when you need to.
+- **Minimal assumptions, maximum flexibility**: Kemal does not force a specific ORM, template engine, or project layout. You are free to choose the tools that fit your application and your team.
+- **Batteries within reason**: Kemal ships with the essentials (routing, middleware, templates, static files, request/response helpers) while keeping advanced concerns in separate shards you can opt into as your app grows.
+
+Kemal is designed to feel familiar if you come from popular web frameworks, while embracing Crystal's strengths and keeping your application code straightforward, maintainable, and ready for production.
+
+## Learning Resources
+
+- π [Official Documentation](http://kemalcr.com)
+- π» [Example Applications](https://github.com/kemalcr/kemal/tree/master/examples)
+- π [Kemal Guide](http://kemalcr.com/guide/)
+- π¬ [Community Chat](https://discord.gg/prSVAZJEpz)
+
+
+## Contributing
+
+We love contributions! Please read our [Contributing Guide](CONTRIBUTING.md) to get started.
+
+## Acknowledgments
+
+Special thanks to Manas for their work on [Frank](https://github.com/manastech/frank).
+
+## License
+
+Kemal is released under the MIT License.
diff --git a/examples/cookies/app.cr b/examples/cookies/app.cr
new file mode 100644
index 0000000..644e01a
--- /dev/null
+++ b/examples/cookies/app.cr
@@ -0,0 +1,67 @@
+require "kemal"
+
+# This example demonstrates different ways to work with cookies in Kemal
+
+# Route to set various types of cookies
+get "/set-cookies" do |env|
+ # Basic cookie with just name and value
+ basic_cookie = HTTP::Cookie.new(
+ name: "BasicCookie",
+ value: "Hello from Kemal!"
+ )
+
+ # Secure cookie with additional security options
+ secure_cookie = HTTP::Cookie.new(
+ name: "SecureCookie",
+ value: "Sensitive Data",
+ http_only: true, # Cookie cannot be accessed via JavaScript
+ secure: true, # Cookie only sent over HTTPS
+ path: "/", # Cookie available for all paths
+ expires: Time.local + Time::Span.new(days: 7) # Cookie expires in 7 days
+ )
+
+ # Session cookie that expires when browser closes
+ session_cookie = HTTP::Cookie.new(
+ name: "SessionCookie",
+ value: "Temporary",
+ http_only: true
+ )
+
+ # Add all cookies to response
+ env.response.cookies << basic_cookie
+ env.response.cookies << secure_cookie
+ env.response.cookies << session_cookie
+
+ "Cookies have been set! Visit /show-cookies to view them."
+end
+
+# Route to display current cookies
+get "/show-cookies" do |env|
+ cookies = env.request.cookies
+ response = String.build do |str|
+ str << "Current Cookies:
"
+ str << ""
+ cookies.each do |cookie|
+ str << "- #{cookie.name}: #{cookie.value}
"
+ end
+ str << "
"
+ end
+ response
+end
+
+# Route to delete a specific cookie
+get "/delete-cookie/:name" do |env|
+ cookie_name = env.params.url["name"]
+
+ # Set cookie with immediate expiration to delete it
+ delete_cookie = HTTP::Cookie.new(
+ name: cookie_name,
+ value: "",
+ expires: Time.local - 1.day
+ )
+
+ env.response.cookies << delete_cookie
+ "Cookie '#{cookie_name}' has been deleted!"
+end
+
+Kemal.run
diff --git a/examples/cors/app.cr b/examples/cors/app.cr
new file mode 100644
index 0000000..ae5b067
--- /dev/null
+++ b/examples/cors/app.cr
@@ -0,0 +1,17 @@
+require "kemal"
+
+# Configure headers for static files using Kemal's static_headers helper
+static_headers do |response, filepath, filestat|
+ # For HTML files, add CORS header to allow requests from example.com
+ # This restricts access to HTML files to only that domain
+ if filepath =~ /\.html$/
+ response.headers.add("Access-Control-Allow-Origin", "example.com")
+ end
+
+ # Add Content-Size header for all static files
+ # This helps clients know the file size before downloading
+ response.headers.add("Content-Size", filestat.size.to_s)
+end
+
+# Start the Kemal web server
+Kemal.run
diff --git a/examples/file-download/app.cr b/examples/file-download/app.cr
new file mode 100644
index 0000000..dd23487
--- /dev/null
+++ b/examples/file-download/app.cr
@@ -0,0 +1,18 @@
+require "kemal"
+
+# Define a route for the root path "/" that will handle file downloads
+get "/" do |env|
+ # Use Kemal's send_file helper to stream a file to the client
+ # Parameters:
+ # - env: The HTTP environment containing request/response data
+ # - "/path/to/your_file": The path to the file you want to download
+ #
+ # send_file will:
+ # - Set appropriate Content-Type header based on file extension
+ # - Stream the file in chunks to handle large files efficiently
+ # - Set Content-Disposition header for browser download behavior
+ send_file env, "/path/to/your_file"
+end
+
+# Start the Kemal web server
+Kemal.run
diff --git a/examples/file-upload/app.cr b/examples/file-upload/app.cr
new file mode 100644
index 0000000..7259ab0
--- /dev/null
+++ b/examples/file-upload/app.cr
@@ -0,0 +1,25 @@
+require "kemal"
+
+# Handle file uploads via POST request to /upload endpoint
+post "/upload" do |env|
+ # Get the uploaded file from the "image" field in the form
+ # The file is initially stored in a temporary location
+ uploaded_file = env.params.files["image"].tempfile
+
+ # Construct the destination path where we'll save the file
+ # - Kemal.config.public_folder is the configured public directory
+ # - "uploads/" is the subdirectory where we'll store uploads
+ # - File.basename gets just the filename from the temp file path
+ uploaded_file_path = ::File.join [Kemal.config.public_folder, "uploads/", File.basename(uploaded_file.path)]
+
+ # Open the destination file for writing and copy the uploaded file to it
+ File.open(uploaded_file_path, "w") do |file|
+ IO.copy(uploaded_file, file)
+ end
+
+ # Return a simple success message
+ "Upload ok"
+end
+
+# Start the Kemal server
+Kemal.run
diff --git a/examples/hello-world/app.cr b/examples/hello-world/app.cr
new file mode 100644
index 0000000..fa687db
--- /dev/null
+++ b/examples/hello-world/app.cr
@@ -0,0 +1,7 @@
+require "kemal"
+
+get "/" do
+ "Hello Kemal!"
+end
+
+Kemal.run
diff --git a/examples/http-basic-auth/app.cr b/examples/http-basic-auth/app.cr
new file mode 100644
index 0000000..5cd2ca3
--- /dev/null
+++ b/examples/http-basic-auth/app.cr
@@ -0,0 +1,18 @@
+require "kemal"
+require "kemal-basic-auth"
+
+# Enable HTTP Basic Authentication
+# This will protect all routes with username/password authentication
+# - username: "username"
+# - password: "password"
+basic_auth "username", "password"
+
+# Define a route for the root path "/"
+get "/" do |_|
+ # This route will only execute if authentication is successful
+ # Otherwise, the browser will show a login prompt
+ "This is shown if basic auth successful."
+end
+
+# Start the Kemal web server
+Kemal.run
diff --git a/examples/http-basic-auth/custom-handler.cr b/examples/http-basic-auth/custom-handler.cr
new file mode 100644
index 0000000..0911973
--- /dev/null
+++ b/examples/http-basic-auth/custom-handler.cr
@@ -0,0 +1,23 @@
+require "kemal-basic-auth"
+
+# Create a custom authentication handler by inheriting from Kemal::BasicAuth::Handler
+class CustomAuthHandler < Kemal::BasicAuth::Handler
+ # Specify which routes should be protected by basic auth
+ # In this case, only /dashboard and /admin routes will require authentication
+ only ["/dashboard", "/admin"]
+
+ # Override the call method to implement custom authentication logic
+ def call(context)
+ # Skip authentication if the current route is not in the protected routes list
+ # This allows other routes to be accessed without authentication
+ return call_next(context) unless only_match?(context)
+
+ # Call the parent class's authentication logic for protected routes
+ # This will prompt for username/password and validate credentials
+ super
+ end
+end
+
+# Register our custom authentication handler with Kemal
+# This enables basic auth for the specified routes
+Kemal.config.auth_handler = CustomAuthHandler
diff --git a/examples/json-api/app.cr b/examples/json-api/app.cr
new file mode 100644
index 0000000..af9ae63
--- /dev/null
+++ b/examples/json-api/app.cr
@@ -0,0 +1,72 @@
+require "kemal"
+require "json"
+
+# Set JSON content type for all routes
+before_all do |env|
+ env.response.content_type = "application/json"
+end
+
+# In-memory storage for users
+USERS = [] of Hash(String, JSON::Any)
+
+# GET - List all users
+get "/users" do |_|
+ USERS.to_json
+end
+
+# GET - Get a specific user by index
+get "/users/:id" do |env|
+ id = env.params.url["id"].to_i
+
+ if id < USERS.size
+ USERS[id].to_json
+ else
+ env.response.status_code = 404
+ {error: "User not found"}.to_json
+ end
+end
+
+# POST - Create a new user
+post "/users" do |env|
+ # Parse request body as JSON
+ # ameba:disable Lint/NotNil
+ user = JSON.parse(env.request.body.not_nil!.gets_to_end)
+ # ameba:enable Lint/NotNil
+ USERS << user.as_h
+
+ env.response.status_code = 201
+ user.to_json
+end
+
+# PUT - Update a user
+put "/users/:id" do |env|
+ id = env.params.url["id"].to_i
+
+ if id < USERS.size
+ # Parse request body as JSON
+ # ameba:disable Lint/NotNil
+ updated_user = JSON.parse(env.request.body.not_nil!.gets_to_end)
+ # ameba:enable Lint/NotNil
+ USERS[id] = updated_user.as_h
+ updated_user.to_json
+ else
+ env.response.status_code = 404
+ {error: "User not found"}.to_json
+ end
+end
+
+# DELETE - Remove a user
+delete "/users/:id" do |env|
+ id = env.params.url["id"].to_i
+
+ if id < USERS.size
+ deleted_user = USERS.delete_at(id)
+ deleted_user.to_json
+ else
+ env.response.status_code = 404
+ {error: "User not found"}.to_json
+ end
+end
+
+# Start the Kemal web server
+Kemal.run
diff --git a/examples/json-mapping/app.cr b/examples/json-mapping/app.cr
new file mode 100644
index 0000000..08b4ba7
--- /dev/null
+++ b/examples/json-mapping/app.cr
@@ -0,0 +1,32 @@
+require "kemal"
+require "json"
+
+# Define a User class that can be created from JSON data
+class User
+ # Include JSON::Serializable to add JSON parsing capabilities
+ # This allows converting JSON strings to User objects and vice versa
+ include JSON::Serializable
+
+ # Define properties that will be mapped from JSON
+ # These properties must match the keys in the incoming JSON
+ property username : String # User's username as a string
+ property password : String # User's password as a string
+end
+
+# Handle POST requests to the root path "/"
+post "/" do |env|
+ # Parse the request body as JSON and create a User object
+ # env.request.body contains the raw JSON data
+ # not_nil! ensures the body exists
+ # User.from_json converts the JSON string to a User object
+ # ameba:disable Lint/NotNil
+ user = User.from_json env.request.body.not_nil!
+ # ameba:enable Lint/NotNil
+
+ # Convert the user object back to JSON and return it
+ # This creates a JSON object with username and password fields
+ {username: user.username, password: user.password}.to_json
+end
+
+# Start the Kemal web server
+Kemal.run
diff --git a/examples/mysql-db/app.cr b/examples/mysql-db/app.cr
new file mode 100644
index 0000000..14045cc
--- /dev/null
+++ b/examples/mysql-db/app.cr
@@ -0,0 +1,56 @@
+require "kemal"
+require "db"
+require "mysql"
+
+# Initialize a single DB connection
+DB_URL = "mysql://root:password@localhost:3306/mydb"
+DBC = DB.open(DB_URL)
+
+# Example User model
+class User
+ include JSON::Serializable # To render json in HTTP::Response
+ include DB::Serializable # To serialize from DB::ResultSet
+
+ property id : Int32
+ property name : String
+ property email : String
+
+ def initialize(@id, @name, @email)
+ end
+end
+
+# List all users
+get "/users" do |_|
+ # Serialize ResultSet
+ users = User.from_rs(DBC.query("SELECT * FROM users"))
+
+ # Return users array as JSON response
+ users.to_json
+end
+
+# Create a new user
+post "/users" do |env|
+ name = env.params.json["name"].as(String)
+ email = env.params.json["email"].as(String)
+
+ user = User.from_rs(DBC.query("INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email", name, email)).first
+
+ {message: "User created with id: #{user.id}"}.to_json
+end
+
+# Delete a user
+delete "/users/:id" do |env|
+ id = env.params.url["id"].to_i
+
+ # Delete user and check if any rows were affected
+ result = DBC.exec "DELETE FROM users WHERE id = ?", id
+
+ if result.rows_affected > 0
+ {message: "User deleted successfully"}.to_json
+ else
+ env.response.status_code = 404
+ {message: "User not found"}.to_json
+ end
+end
+
+Kemal.run
diff --git a/examples/postgresql-db/app.cr b/examples/postgresql-db/app.cr
new file mode 100644
index 0000000..01aa629
--- /dev/null
+++ b/examples/postgresql-db/app.cr
@@ -0,0 +1,56 @@
+require "kemal"
+require "db"
+require "pg"
+
+# Initialize a single DB connection
+DB_URL = "postgres://postgres:postgres@localhost:5432/mydb"
+DBC = DB.open(DB_URL)
+
+# Example User model
+class User
+ include JSON::Serializable # To render json in HTTP::Response
+ include DB::Serializable # To serialize from DB::ResultSet
+
+ property id : Int32
+ property name : String
+ property email : String
+
+ def initialize(@id, @name, @email)
+ end
+end
+
+# List all users
+get "/users" do |_|
+ # Serialize ResultSet
+ users = User.from_rs(DBC.query("SELECT * FROM users"))
+
+ # Return users array as JSON response
+ users.to_json
+end
+
+# Create a new user
+post "/users" do |env|
+ name = env.params.json["name"].as(String)
+ email = env.params.json["email"].as(String)
+
+ user = User.from_rs(DBC.query("INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email", name, email)).first
+
+ {message: "User created with id: #{user.id}"}.to_json
+end
+
+# Delete a user
+delete "/users/:id" do |env|
+ id = env.params.url["id"].to_i
+
+ # Delete user and check if any rows were affected
+ result = DBC.exec "DELETE FROM users WHERE id = $1", id
+
+ if result.rows_affected > 0
+ {message: "User deleted successfully"}.to_json
+ else
+ env.response.status_code = 404
+ {message: "User not found"}.to_json
+ end
+end
+
+Kemal.run
diff --git a/examples/redis/app.cr b/examples/redis/app.cr
new file mode 100644
index 0000000..c7a8492
--- /dev/null
+++ b/examples/redis/app.cr
@@ -0,0 +1,58 @@
+require "kemal"
+require "redis"
+
+# Initialize Redis client
+REDIS = Redis.new(host: "localhost", port: 6379)
+
+# Store a value
+post "/store/:key" do |env|
+ key = env.params.url["key"]
+ value = env.params.json["value"].as(String)
+
+ REDIS.set(key, value)
+ {message: "Value stored successfully"}.to_json
+end
+
+# Retrieve a value
+get "/get/:key" do |env|
+ key = env.params.url["key"]
+
+ if value = REDIS.get(key)
+ {key: key, value: value}.to_json
+ else
+ env.response.status_code = 404
+ {message: "Key not found"}.to_json
+ end
+end
+
+# Delete a value
+delete "/:key" do |env|
+ key = env.params.url["key"]
+
+ if REDIS.del(key) > 0
+ {message: "Key deleted successfully"}.to_json
+ else
+ env.response.status_code = 404
+ {message: "Key not found"}.to_json
+ end
+end
+
+# Increment a counter
+post "/incr/:key" do |env|
+ key = env.params.url["key"]
+ new_value = REDIS.incr(key)
+
+ {key: key, value: new_value}.to_json
+end
+
+# Store with expiration
+post "/store_temp/:key" do |env|
+ key = env.params.url["key"]
+ value = env.params.json["value"].as(String)
+ ttl = env.params.json["ttl"].as(Int64)
+
+ REDIS.setex(key, ttl, value)
+ {message: "Value stored with expiration"}.to_json
+end
+
+Kemal.run
diff --git a/examples/reuse-port/app.cr b/examples/reuse-port/app.cr
new file mode 100644
index 0000000..b059252
--- /dev/null
+++ b/examples/reuse-port/app.cr
@@ -0,0 +1,19 @@
+require "kemal"
+
+# Define a simple route that returns a message
+get "/" do
+ "Reusing port 3000"
+end
+
+# Start Kemal with custom server configuration
+Kemal.run do |config|
+ # Get the server instance from the config
+ # ameba:disable Lint/NotNil
+ server = config.server.not_nil!
+ # ameba:enable Lint/NotNil
+
+ # Bind the server to port 3000 with reuse_port enabled
+ # reuse_port: true allows multiple processes to listen on the same port
+ # This is useful for load balancing across multiple worker processes
+ server.bind_tcp "0.0.0.0", 3000, reuse_port: true
+end
diff --git a/examples/unix-domain-socket/app.cr b/examples/unix-domain-socket/app.cr
new file mode 100644
index 0000000..aae5973
--- /dev/null
+++ b/examples/unix-domain-socket/app.cr
@@ -0,0 +1,14 @@
+require "kemal"
+
+# Start Kemal with custom server configuration to use Unix Domain Socket
+Kemal.run do |config|
+ # Get the server instance from the config
+ # ameba:disable Lint/NotNil
+ server = config.server.not_nil!
+ # ameba:enable Lint/NotNil
+
+ # Bind the server to a Unix Domain Socket instead of TCP port
+ # Unix Domain Sockets provide faster inter-process communication on the same machine
+ # They are commonly used when the client and server are on the same host
+ server.bind_unix "path/to/socket.sock"
+end
diff --git a/examples/websocket-chat/app.cr b/examples/websocket-chat/app.cr
new file mode 100644
index 0000000..b7e380d
--- /dev/null
+++ b/examples/websocket-chat/app.cr
@@ -0,0 +1,33 @@
+require "kemal"
+
+# Array to store chat message history
+messages = [] of String
+# Array to keep track of connected WebSocket clients
+sockets = [] of HTTP::WebSocket
+
+# Create WebSocket endpoint at root path "/"
+ws "/" do |socket|
+ # Add newly connected client socket to our sockets array
+ sockets.push socket
+
+ # Handle incoming messages from clients
+ socket.on_message do |message|
+ # Store the new message in history
+ messages.push message
+ # Broadcast the updated message history to all connected clients
+ sockets.each do |a_socket|
+ a_socket.send messages.to_json
+ end
+ end
+
+ # Handle client disconnection
+ socket.on_close do |_|
+ # Remove disconnected client's socket from our array
+ sockets.delete(socket)
+ # Log disconnection event
+ puts "Closing Socket: #{socket}"
+ end
+end
+
+# Start the Kemal server
+Kemal.run
diff --git a/samples/hello_world.cr b/samples/hello_world.cr
deleted file mode 100644
index c04f1d5..0000000
--- a/samples/hello_world.cr
+++ /dev/null
@@ -1,8 +0,0 @@
-require "kemal"
-
-# Set root. If not specified the default content_type is 'text'
-get "/" do
- "Hello Kemal!"
-end
-
-Kemal.run
diff --git a/samples/json_api.cr b/samples/json_api.cr
deleted file mode 100644
index 0132c14..0000000
--- a/samples/json_api.cr
+++ /dev/null
@@ -1,11 +0,0 @@
-require "kemal"
-require "json"
-
-# You can easily access the context and set content_type like 'application/json'.
-# Look how easy to build a JSON serving API.
-get "/" do |env|
- env.response.content_type = "application/json"
- {name: "Serdar", age: 27}.to_json
-end
-
-Kemal.run
diff --git a/samples/websocket_server.cr b/samples/websocket_server.cr
deleted file mode 100644
index 61a0802..0000000
--- a/samples/websocket_server.cr
+++ /dev/null
@@ -1,11 +0,0 @@
-require "kemal"
-
-ws "/" do |socket|
- socket.send "Hello from Kemal!"
-
- socket.on_message do |message|
- socket.send "Echo back from server #{message}"
- end
-end
-
-Kemal.run
diff --git a/shard.yml b/shard.yml
index d0371c4..e23257b 100644
--- a/shard.yml
+++ b/shard.yml
@@ -1,5 +1,5 @@
name: kemal
-version: 1.6.0
+version: 1.10.1
authors:
- Serdar Dogruyol
@@ -15,6 +15,7 @@ dependencies:
development_dependencies:
ameba:
github: crystal-ameba/ameba
+ branch: master
crystal: ">= 0.36.0"
diff --git a/spec/asset/hello_with_content_for.ecr b/spec/asset/hello_with_content_for.ecr
index b5460f9..8b4dc69 100644
--- a/spec/asset/hello_with_content_for.ecr
+++ b/spec/asset/hello_with_content_for.ecr
@@ -2,4 +2,4 @@ Hello <%= name %>
<% content_for "meta" do %>
Kemal Spec
-<% end %>
\ No newline at end of file
+<% end %>
diff --git a/spec/asset/layout_with_yield.ecr b/spec/asset/layout_with_yield.ecr
index 3710c4a..a025b2a 100644
--- a/spec/asset/layout_with_yield.ecr
+++ b/spec/asset/layout_with_yield.ecr
@@ -5,4 +5,4 @@
<%= content %>
-