crooner
| Kind | kit |
|---|---|
| Capabilities | http file |
| Categories | web network |
| Keywords | web framework http server routing sinatra |
A Sinatra-inspired web framework for Kit
Files
| File | Description |
|---|---|
.editorconfig | Editor formatting configuration |
.gitignore | Git ignore rules for build artifacts and dependencies |
.tool-versions | asdf tool versions (Zig, Kit) |
LICENSE | MIT license file |
README.md | This file |
examples/components-demo.kit | Example: components demo |
examples/csrf-protection.kit | Example: csrf protection |
examples/filters.kit | Example: filters |
examples/hello.kit | Example: hello |
examples/htmx/htmx-demo.kit | Example: htmx demo |
examples/htmx/public/style.css | style.css |
examples/http.kit | Example: http |
examples/middleware.kit | Example: middleware |
examples/public/app.js | app.js |
examples/public/style.css | style.css |
examples/simple-server.kit | Example: simple server |
examples/static-files.kit | Example: static files |
examples/tcp.kit | Example: tcp |
examples/welcome-test.kit | Example: welcome test |
kit.toml | Package manifest with metadata and dependencies |
src/components.kit | Module for components |
src/main.kit | Crooner error type for typed error handling. |
src/request.kit | Create an empty request record. |
src/response.kit | Get the standard text description for an HTTP status code. |
src/router.kit | Module for router |
src/server.kit | Module for server |
tests/components.test.kit | Tests for components |
tests/crooner.test.kit | Tests for crooner |
Architecture
Request Lifecycle
Module Structure
Dependencies
loggingtemplate
Installation
kit add gitlab.com/kit-lang/packages/kit-crooner.gitUsage
import Kit.CroonerLicense
MIT License - see LICENSE for details.
Exported Functions & Types
CroonerError
Crooner error type for typed error handling. Variants distinguish between different failure modes.
Variants
CroonerServerError {message}CroonerConnectionError {message}CroonerRouteError {message}CroonerRequestError {message}app
Create a Crooner application with default configuration.
Returns a new application record with empty routes, error handlers, and filters, and a default compact-format logger.
() -> App
app = Crooner.app
|> Crooner.get "/" fn(req) => {body: "Hello!"}app-with-options
Create a Crooner application with custom logger configuration.
Allows customization of the logger format and request event logging. Supported formats are from the Logging module (CompactFmt, JsonFmt, etc.).
{logger-format: LogFormat, request-events: Bool} -> App
# Enable request events for detailed request logging
app = Crooner.app-with-options {request-events: true}
# Combine with JSON format for production
app = Crooner.app-with-options {logger-format: :json, request-events: true}with-logger
Set or replace the logger on an existing app.
Useful for integrating a custom logger or changing logging configuration after app creation.
App -> Logger -> App
custom-logger = make-default-logger my-config
app = Crooner.app |> Crooner.with-logger custom-loggerwith-request-events
Enable request-scoped event logging.
When enabled, each request gets a Logging event attached to req.event that accumulates context throughout the request lifecycle. Handlers and filters can add fields to the event, and a single comprehensive log entry is emitted after the response is sent.
App -> App
app = Crooner.app |> Crooner.with-request-events
# In your handler, access the event:
handler = fn(req) =>
req.event |> Map.insert "user-id" 42
{status: 200, body: "OK"}with-event-format
Set the format for request event logging.
Controls how request events are formatted when emitted. Use this to match the event format to your application's logging style.
App -> LogFormat -> App
app = Crooner.app
|> Crooner.with-logger my-logger
|> Crooner.with-event-format :pretty
|> Crooner.with-request-eventsget
Register a GET route handler.
Registers a handler function for HTTP GET requests matching the pattern. Patterns can include path parameters using :name syntax.
App -> String -> (Request -> Response) -> App
app |> Crooner.get "/users/:id" fn(req) =>
id = Crooner.get-param req "id" |> Option.unwrap "0"
{body: "User ${id}"}route-get
Register a GET route handler (alias for get).
Use this when get conflicts with other imports (e.g., HTTP.get). Functionally identical to Crooner.get.
App -> String -> (Request -> Response) -> App
post
Register a POST route handler.
App -> String -> (Request -> Response) -> App
put
Register a PUT route handler.
App -> String -> (Request -> Response) -> App
delete
Register a DELETE route handler.
App -> String -> (Request -> Response) -> App
patch
Register a PATCH route handler.
App -> String -> (Request -> Response) -> App
head-request
Register a HEAD route handler.
App -> String -> (Request -> Response) -> App
options
Register an OPTIONS route handler.
App -> String -> (Request -> Response) -> App
error
Register a custom error handler for a specific status code.
Allows customization of error responses for specific HTTP status codes.
App -> Int -> (Request -> Response) -> App
app |> Crooner.error 404 fn(req) =>
{status: 404, body: "Page not found: ${req.path}"}before
Register a before filter that runs before route handlers.
Before filters can modify the request or short-circuit by returning a response. They execute in registration order.
App -> (Request -> Request | Response) -> App
auth-filter = fn(req) =>
match Crooner.get-header req "Authorization"
| Some _ -> req # Continue with request
| None -> {status: 401, body: "Unauthorized"} # Short-circuit
app |> Crooner.before auth-filterafter
Register an after filter that runs after route handlers.
After filters can modify the response before it's sent to the client. They execute in registration order and receive both request and response.
App -> (Request -> Response -> Response) -> App
cors-filter = fn(req, resp) =>
# Add CORS headers to response
{status: resp.status, body: resp.body,
headers: Map.insert "Access-Control-Allow-Origin" "*" resp.headers}
app |> Crooner.after cors-filterstatic
Register a static file serving route for a URL prefix and filesystem path.
Maps a URL prefix to a filesystem directory for serving static files. Automatically sets appropriate Content-Type headers based on file extensions.
App -> NonEmptyString -> NonEmptyString -> App
app |> Crooner.static "/assets" "./public/assets"
# Now /assets/style.css serves ./public/assets/style.cssuse
Add Ring-style middleware that wraps the request/response cycle.
Ring-style middleware is a higher-order function: (handler -> handler). Middleware forms a chain where each wraps the next, creating an "onion" model. First middleware added is the outermost layer.
App -> ((Request -> Response) -> (Request -> Response)) -> App
wrap-logging = fn(handler) =>
fn(req) =>
println "Before: ${req.path}"
resp = handler req
println "After: ${resp.status}"
resp
app |> Crooner.use wrap-loggingcsrf-protect
Add CSRF protection using Sec-Fetch-Site header validation.
Uses the Fetch Metadata standard (Sec-Fetch-Site header) to validate requests. Only state-changing methods (POST, PUT, DELETE, PATCH) are checked. Safe values are: "same-origin", "same-site", "none".
App -> {exclude-paths: List String} -> App
app |> Crooner.csrf-protect {exclude-paths: ["/api/webhook"]}csrf-protect-simple
Add simple CSRF protection with no exclusions.
Convenience function for adding CSRF protection without any excluded paths.
App -> App
app |> Crooner.csrf-protect-simplelisten
Start the server on all interfaces (0.0.0.0).
Binds to 0.0.0.0, making the server accessible from any network interface. Does not return (enters server loop).
App -> PositiveInt -> (() -> a) -> ()
Crooner.listen app 3000 fn =>
println "Server started on http://0.0.0.0:3000"listen-local
Start the server on localhost only (127.0.0.1).
Binds to 127.0.0.1, making the server only accessible from the local machine. Useful for development or when running behind a reverse proxy. Does not return (enters server loop).
App -> PositiveInt -> (() -> a) -> ()
Crooner.listen-local app 3000 fn =>
println "Server started on http://127.0.0.1:3000"ok
Create a 200 OK response with the given body.
String -> Response
created
Create a 201 Created response with the given body.
String -> Response
no-content
Create a 204 No Content response.
() -> Response
bad-request
Create a 400 Bad Request response with the given body.
String -> Response
not-found
Create a 404 Not Found response with the given body.
String -> Response
internal-error
Create a 500 Internal Server Error response with the given body.
String -> Response
redirect
Create a 302 redirect response to the given URL.
String -> Response
redirect-permanent
Create a 301 permanent redirect response to the given URL.
String -> Response
json-response
Create a JSON response with the given status and body. Sets Content-Type to application/json; charset=utf-8.
Int -> String -> Response
html-response
Create an HTML response with the given status and body. Sets Content-Type to text/html; charset=utf-8.
Int -> String -> Response
text-response
Create a plain text response with the given status and body. Sets Content-Type to text/plain; charset=utf-8.
Int -> String -> Response
get-header
Get a header value from a request. Header lookup is case-sensitive as stored in the request.
Request -> NonEmptyString -> Option String
get-query
Get a query parameter value from a request. Extracts a value from the URL query string (e.g., ?key=value).
Request -> NonEmptyString -> Option String
get-query-param
Alias for get-query (backwards compatibility).
Request -> NonEmptyString -> Option String
get-param
Get a path parameter value from a request. Extracts a value from a path pattern parameter (e.g., :id in /users/:id).
Request -> NonEmptyString -> Option String
# Route: /users/:id
# Request: /users/42
id = Crooner.get-param req "id" # Some "42"get-event
Get the logging event from a request. When request-events are enabled, returns the event that can be enriched with additional context throughout the request lifecycle.
Request -> Option Event
handler = fn(req) =>
evt = Crooner.get-event req |> Option.unwrap
evt |> Map.insert "user-id" 42
evt |> Map.insert "db-query-ms" 15
{status: 200, body: "OK"}has-event?
Check if a request has a logging event attached. Returns true if request has an event (request-events enabled), false otherwise.
Request -> Bool
default-config
Get the default server configuration.
Returns: Default configuration with host "0.0.0.0" and port 3000.
Config
config = default-config
# {host: "0.0.0.0", port: 3000}handle-request
Handle a single HTTP request by routing and sending the response.
Takes the parsed HTTP request from HTTP.server-accept, routes it through the router, and sends back the HTTP response via HTTP.server-respond.
When request-events are enabled, creates a logging event at the start of the request, attaches it to the request object, and emits a comprehensive log entry after the response is sent.
Parameters:
router - Router record with routes and handlershttp-request - Parsed HTTP request from HTTP.server-accept
Returns: Unit (connection is closed after responding)
Router -> HttpRequest -> Unit
handle-request router http-requestaccept-loop
Accept and handle HTTP requests in a loop.
Continuously accepts incoming HTTP requests and handles each one. This is a blocking loop that runs until the server is stopped.
Parameters:
router - Router record with routes and handlersserver-handle - HTTP server handle from HTTP.server-create
Returns: Never returns (infinite loop)
Router -> Ptr -> Unit
accept-loop router server-handlestart
Start the server with the given configuration.
Creates an HTTP server and starts accepting client connections. Calls the on-start callback after the server is created successfully.
Parameters:
router - Router record with routes and handlersconfig - Configuration record with host and port fieldson-start - Callback function to run after server starts
Returns: Never returns (enters accept loop)
Router -> Config -> (() -> ()) -> Unit
config = {host: "127.0.0.1", port: 8080}
start router config fn =>
println "Server started!"listen
Start the server on all interfaces (0.0.0.0).
Convenience function that starts the server listening on all network interfaces.
Parameters:
router - Router record with routes and handlersport - Port number to listen onon-start - Callback function to run after server starts
Returns: Never returns (enters accept loop)
Router -> PositiveInt -> (() -> ()) -> Unit
listen router 3000 fn =>
println "Listening on port 3000"listen-local
Start the server on localhost only (127.0.0.1).
Convenience function that starts the server listening only on localhost. Useful for development or when the server should only accept local connections.
Parameters:
router - Router record with routes and handlersport - Port number to listen onon-start - Callback function to run after server starts
Returns: Never returns (enters accept loop)
Router -> PositiveInt -> (() -> ()) -> Unit
listen-local router 3000 fn =>
println "Listening on localhost:3000"status-text
Get the standard text description for an HTTP status code.
Parameters:
code - HTTP status code (e.g., 200, 404, 500)
Returns: Standard status text for the code (e.g., "OK", "Not Found")Returns "Unknown" for unrecognized codes.
Int -> String
text = status-text 404 # "Not Found"response
Create a basic response with status and body.
Creates a plain text response with the given status code and body. Default content-type is "text/plain; charset=utf-8".
Parameters:
status - HTTP status codebody - Response body string
Returns: Response record with kind, status, headers, and body fields.
Int -> String -> Response
resp = response 200 "Success"from-handler-result
Convert a handler result (string or record) into a normalized response.
Normalizes different handler return values into a standard response record. Accepts strings, partial response records, or full response records.
Parameters:
result - Handler return value (string or record)
Returns: Normalized Response record with all required fields.
a -> Response
# String result
resp = from-handler-result "Hello" # 200 OK with body "Hello"
# Partial record
resp = from-handler-result {body: "OK"} # 200 OK with body "OK"
# Full record
resp = from-handler-result {status: 201, body: "Created", headers: {}}build
Build the raw HTTP response string from a response record.
Converts a response record into a raw HTTP response string suitable for sending over TCP. Automatically adds Content-Length header based on body size.
Parameters:
resp - Response record with status, body, and headers
Returns: Raw HTTP response string with status line, headers, and body.
Response -> String
raw = build {kind: "response", status: 200, body: "OK", headers: {}}
# "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK"to-http-response
Convert a Crooner response to HTTP server response format.
Converts a Crooner response record (with headers as a Record) into the format expected by HTTP.server-respond (with headers as a list of records).
Parameters:
resp - Crooner response record with kind, status, headers (Record), and body
Returns: HTTP server response record with status, body, and headers as [{name, value}]
Response -> HttpResponse
crooner-resp = ok "Hello"
http-resp = to-http-response crooner-resp
HTTP.server-respond handle http-respok
Create a 200 OK response.
Parameters:
body - Response body string
Returns: Response with status 200
String -> Response
created
Create a 201 Created response.
Parameters:
body - Response body string
Returns: Response with status 201
String -> Response
no-content
Create a 204 No Content response.
Returns: Response with status 204 and empty body
Unit -> Response
bad-request
Create a 400 Bad Request response.
Parameters:
body - Response body string
Returns: Response with status 400
String -> Response
unauthorized
Create a 401 Unauthorized response.
Parameters:
body - Response body string
Returns: Response with status 401
String -> Response
forbidden
Create a 403 Forbidden response.
Parameters:
body - Response body string
Returns: Response with status 403
String -> Response
not-found
Create a 404 Not Found response.
Parameters:
body - Response body string
Returns: Response with status 404
String -> Response
method-not-allowed
Create a 405 Method Not Allowed response.
Parameters:
body - Response body string
Returns: Response with status 405
String -> Response
internal-error
Create a 500 Internal Server Error response.
Parameters:
body - Response body string
Returns: Response with status 500
String -> Response
redirect
Create a 302 temporary redirect response.
Parameters:
url - URL to redirect to
Returns: Response with status 302 and Location header
String -> Response
resp = redirect "/login"redirect-permanent
Create a 301 permanent redirect response.
Parameters:
url - URL to redirect to
Returns: Response with status 301 and Location header
String -> Response
resp = redirect-permanent "/new-location"json-response
Create a JSON response with the given status and body.
Parameters:
status - HTTP status codebody - JSON string
Returns: Response with application/json content-type
Int -> String -> Response
resp = json-response 200 "{\"status\":\"ok\"}"html-response
Create an HTML response with the given status and body.
Parameters:
status - HTTP status codebody - HTML string
Returns: Response with text/html content-type
Int -> String -> Response
resp = html-response 200 "<h1>Hello</h1>"text-response
Create a plain text response with the given status and body.
Parameters:
status - HTTP status codebody - Plain text string
Returns: Response with text/plain content-type
Int -> String -> Response
resp = text-response 200 "Hello, World!"ComponentError
Error type for component operations
Variants
PropMissing {comp-name, prop}PropTypeError {comp-name, prop, expected}RenderError {comp-name, message}LoadError {path, message}PropType
Prop type definitions for validation
Variants
StringPropBoolPropIntPropListPropObjectPropPropDef
Prop definition - required or optional with default
Variants
Required {PropType}Optional {PropType, String}registry
Create an empty component registry
() -> List (String, Record)
register
Register a component in the registry
List (String, Record) -> Record -> List (String, Record)
get
Get a component from the registry by name
List (String, Record) -> NonEmptyString -> Option Record
make-component
Create a component with the given name, template, props, and slots
NonEmptyString -> String -> List (String, PropDef) -> List String -> Record
component
Create a simple component with just name and template
NonEmptyString -> String -> Record
component-with-props
Create a component with name, template, and props
NonEmptyString -> String -> List (String, PropDef) -> Record
load
Load a component template from a file
NonEmptyString -> NonEmptyString -> Result Record ComponentError
load-with-props
Load a component template from a file with prop and slot definitions
NonEmptyString -> NonEmptyString -> List (String, PropDef) -> List String -> Result Record ComponentError
render
Render a component with the given props
Record -> Record -> Result String ComponentError
render-with-slots
Render a component with props and named slots
Record -> Record -> Record -> Result String ComponentError
render-unsafe
Render a component unsafely - returns empty string on error and logs the error Useful for prototyping where you don't want to handle errors
Record -> Record -> String
render-list
Render a component for each item in a list The mapper function transforms each item into props for the component
Record -> List a -> (a -> Record) -> Result String ComponentError
render-list-or-empty
Render a component for each item in a list, with fallback for empty list
Record -> List a -> (a -> Record) -> String -> Result String ComponentError
load-all
Load all templates from a directory with the given extension Returns a registry with components named after their files (without extension)
String -> String -> Result Registry ComponentError
empty-request
Create an empty request record.
Returns: An empty Request record with default values for all fields.
Unit -> Request
req = empty-request
# req.method == ""
# req.path == ""from-http-request
Convert an HTTP server request to a Crooner request record.
This function is used internally by the server to convert the raw HTTP server request format from HTTP.server-accept into Crooner's structured request format.
Parameters:
http-request - Request from HTTP.server-accept with format-{method, path, headers- [{name, value}], body, handle}
Returns: Crooner request record with headers as Map and query params parsed
HttpRequest -> Request
http-req = HTTP.server-accept server-handle
req = from-http-request http-reqparse-request
Parse a raw HTTP request string into a request record.
Parses the full HTTP request including request line, headers, and body. Automatically extracts query parameters from the URL and separates the request body from headers.
Parameters:
raw - Raw HTTP request string with CRLF line endings
Returns: Request record with parsed method, path, version, headers, query, and body.Returns empty-request if parsing fails.
String -> Request
raw = "POST /api/users HTTP/1.1\r\nContent-Type: application/json\r\n\r\n{\"name\":\"Alice\"}"
req = parse-request raw
# req.method == "POST"
# req.path == "/api/users"
# req.body == "{\"name\":\"Alice\"}"get-header
Get a header value from the request by name.
Parameters:
req - Request recordname - Header name (case-sensitive)
Returns: Some value if header exists, None otherwise
Request -> NonEmptyString -> Option String
content-type = get-header req "Content-Type"get-query
Get a query parameter value from the request by name.
Parameters:
req - Request recordname - Query parameter name
Returns: Some value if query parameter exists, None otherwise
Request -> NonEmptyString -> Option String
filter = get-query req "filter"get-param
Get a path parameter value from the request by name.
Path parameters are extracted from URL patterns like "/users/:id".
Parameters:
req - Request recordname - Path parameter name (without the colon prefix)
Returns: Some value if path parameter exists, None otherwise
Request -> NonEmptyString -> Option String
# For route "/users/:id" and path "/users/123"
user-id = get-param req "id" # Some "123"is-method?
Check if the request method matches the given method.
Parameters:
req - Request recordmethod - HTTP method to check (e.g., "GET", "POST")
Returns: true if request method matches, false otherwise
Request -> String -> Bool
if is-method? req "POST" then
# Handle POST requestis-get?
Check if the request method is GET.
Parameters:
req - Request record
Returns: true if method is GET, false otherwise
Request -> Bool
is-post?
Check if the request method is POST.
Parameters:
req - Request record
Returns: true if method is POST, false otherwise
Request -> Bool
is-put?
Check if the request method is PUT.
Parameters:
req - Request record
Returns: true if method is PUT, false otherwise
Request -> Bool
is-delete?
Check if the request method is DELETE.
Parameters:
req - Request record
Returns: true if method is DELETE, false otherwise
Request -> Bool
is-patch?
Check if the request method is PATCH.
Parameters:
req - Request record
Returns: true if method is PATCH, false otherwise
Request -> Bool
attach-event
Attach a logging event to a request.
Used internally by the server when request-events are enabled. The event accumulates context throughout the request lifecycle.
Parameters:
req - Request recordevent - Logging event from Logging.event
Returns: Request record with event attached
Request -> Event -> Request
req = attach-event req eventget-event
Get the logging event from a request.
Returns the event if request-events are enabled, None otherwise.
Parameters:
req - Request record
Returns: Option Event - Some event if attached, None otherwise
Request -> Option Event
match get-event req
| Some evt -> evt |> Logging.add-field "key" "value"
| None ->has-event?
Check if a request has an event attached.
Parameters:
req - Request record
Returns: true if request has an event, false otherwise
Request -> Bool