crooner

A Sinatra-inspired web framework for Kit

Files

FileDescription
.editorconfigEditor formatting configuration
.gitignoreGit ignore rules for build artifacts and dependencies
.tool-versionsasdf tool versions (Zig, Kit)
LICENSEMIT license file
README.mdThis file
examples/components-demo.kitExample: components demo
examples/csrf-protection.kitExample: csrf protection
examples/filters.kitExample: filters
examples/hello.kitExample: hello
examples/htmx/htmx-demo.kitExample: htmx demo
examples/htmx/public/style.cssstyle.css
examples/http.kitExample: http
examples/middleware.kitExample: middleware
examples/public/app.jsapp.js
examples/public/style.cssstyle.css
examples/simple-server.kitExample: simple server
examples/static-files.kitExample: static files
examples/tcp.kitExample: tcp
examples/welcome-test.kitExample: welcome test
kit.tomlPackage manifest with metadata and dependencies
src/components.kitModule for components
src/main.kitCrooner error type for typed error handling.
src/request.kitCreate an empty request record.
src/response.kitGet the standard text description for an HTTP status code.
src/router.kitModule for router
src/server.kitModule for server
tests/components.test.kitTests for components
tests/crooner.test.kitTests for crooner

Architecture

Request Lifecycle

sequenceDiagram participant Client participant Server participant Router participant Middleware participant Handler Client->>Server: HTTP Request Server->>Router: Match Route Router->>Middleware: Before Filters Middleware->>Handler: Route Handler Handler->>Middleware: Response Middleware->>Server: After Filters Server->>Client: HTTP Response

Module Structure

graph TD A[main.kit] --> B[server.kit] A --> C[router.kit] A --> D[request.kit] A --> E[response.kit] A --> F[components.kit] B --> G[TCP/HTTP] F --> H[template]

Dependencies

  • logging
  • template

Installation

kit add gitlab.com/kit-lang/packages/kit-crooner.git

Usage

import Kit.Crooner

License

MIT License - see LICENSE for details.

Exported Functions & Types

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 handlers
  • http-request - Parsed HTTP request from HTTP.server-accept

Returns: Unit (connection is closed after responding)

Router -> HttpRequest -> Unit

handle-request router http-request

accept-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 handlers
  • server-handle - HTTP server handle from HTTP.server-create

Returns: Never returns (infinite loop)

Router -> Ptr -> Unit

accept-loop router server-handle

start

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 handlers
  • config - Configuration record with host and port fields
  • on-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 handlers
  • port - Port number to listen on
  • on-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 handlers
  • port - Port number to listen on
  • on-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 code
  • body - 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-resp

ok

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 code
  • body - 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 code
  • body - 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 code
  • body - 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

StringProp
BoolProp
IntProp
ListProp
ObjectProp

PropDef

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-req

parse-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 record
  • name - 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 record
  • name - 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 record
  • name - 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 record
  • method - 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 request

is-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 record
  • event - Logging event from Logging.event

Returns: Request record with event attached

Request -> Event -> Request

req = attach-event req event

get-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