crooner

A Sinatra-inspired web framework for Kit

Files

FileDescription
kit.tomlPackage manifest with metadata and dependencies
src/components.kitHTML component helpers and template rendering
src/main.kitMain module - exports all public functions and types
src/request.kitHTTP request parsing and headers
src/response.kitHTTP response building and status codes
src/router.kitURL routing with pattern matching and filters
src/server.kitTCP server and HTTP lifecycle management
tests/components.test.kitTests for HTML component generation
tests/crooner.test.kitCore framework integration tests
examples/components-demo.kitHTML component and template usage
examples/csrf-protection.kitCSRF token validation middleware
examples/filters.kitBefore/after filter pipeline demo
examples/hello.kitMinimal "Hello World" web server
examples/http.kitHTTP client request examples
examples/middleware.kitCustom middleware chain setup
examples/simple-server.kitBasic HTTP server with routes
examples/static-files.kitServing static files from disk
examples/tcp.kitLow-level TCP socket handling
examples/welcome-test.kitWelcome page response test
LICENSEMIT license file

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

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: Logging.JsonFmt, 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 = Logging.logger my-config
app = Crooner.app |> Crooner.with-logger custom-logger

with-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 |> Logging.add-field "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 Logging.PrettyFmt
  |> Crooner.with-request-events

get

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

after

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

static

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 -> String -> String -> App

app |> Crooner.static "/assets" "./public/assets"
# Now /assets/style.css serves ./public/assets/style.css

use

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

csrf-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-simple

listen

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 -> Int -> (() -> 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 -> Int -> (() -> 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 -> String -> 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 -> String -> Option String

get-query-param

Alias for get-query (backwards compatibility).

Request -> String -> 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 -> String -> 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 |> Logging.add-field "user-id" 42
  evt |> Logging.add-timing "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 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 -> Int -> (() -> ()) -> 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 -> Int -> (() -> ()) -> 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!"

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

router

Create an empty router with no routes or handlers.

Returns: Empty Router record with empty routes, error-handlers, filters, and middleware lists.

Router

app = router

has-routes?

Check if the router has any routes defined.

Parameters:

  • router - Router record

Returns: true if router has at least one route, false otherwise

Router -> Bool

if has-routes? router then
  # Router has routes

welcome-page

Generate the default welcome page response.

Returns: HTML response with welcome page, shown when no routes are defined.Includes current Kit and Crooner version numbers.

Response

resp = welcome-page

add-route

Add a route to the router with method, pattern, and handler.

Parameters:

  • router - Router record
  • method - HTTP method (e.g., "GET", "POST")
  • pattern - URL pattern with optional path parameters (e.g., "/users/ - id")
  • handler - Handler function that takes a request and returns a response

Returns: Updated Router with the new route added.

Router -> String -> String -> (Request -> Response) -> Router

router = add-route router "GET" "/users/:id" fn(req) =>
  {status: 200, body: "User page"}

add-error-handler

Register a custom error handler for a specific HTTP status code.

Parameters:

  • router - Router record
  • status-code - HTTP status code (e.g., 404, 500)
  • handler - Handler function that takes a request and returns a response

Returns: Updated Router with the custom error handler registered.

Router -> Int -> (Request -> Response) -> Router

router = add-error-handler router 404 fn(req) =>
  {status: 404, body: "Custom not found page"}

add-before-filter

Add a before filter that runs before route handlers.

Before filters can modify requests or short-circuit request processing by returning a response instead of the (possibly modified) request.

Parameters:

  • router - Router record
  • filter - Filter function that takes a request and returns a request or response

Returns: Updated Router with the before filter added.

Router -> (Request -> Request | Response) -> Router

auth-filter = fn(req) =>
  match get-header req "Authorization"
    | Some token -> req  # Continue processing
    | None -> {status: 401, body: "Unauthorized"}  # Short-circuit
router = add-before-filter router auth-filter

add-after-filter

Add an after filter that runs after route handlers.

After filters can modify responses before they are sent to the client.

Parameters:

  • router - Router record
  • filter - Filter function that takes (request, response) and returns a response

Returns: Updated Router with the after filter added.

Router -> (Request -> Response -> Response) -> Router

cors-filter = fn(req, resp) =>
  # Add CORS headers to response
  {kind: "response", status: resp.status, body: resp.body,
   headers: Map.insert "Access-Control-Allow-Origin" "*" resp.headers}
router = add-after-filter router cors-filter

add-static-route

Add a static file serving route for a URL prefix and filesystem path.

Serves files from the filesystem when the URL matches the prefix. Automatically detects MIME types based on file extension.

Parameters:

  • router - Router record
  • url-prefix - URL prefix to match (e.g., "/static")
  • fs-path - Filesystem path to serve files from

Returns: Updated Router with the static route added.

Router -> String -> String -> Router

router = add-static-route router "/static" "./public"
# GET /static/style.css -> serves ./public/style.css

add-middleware

Add Ring-style middleware to wrap the request handling pipeline.

Ring-style middleware is a higher-order function that takes a handler and returns a new handler. Middleware are applied in order, with the first added being the outermost layer.

Parameters:

  • router - Router record
  • mw - Middleware function - (handler -> handler)

Returns: Updated Router with the middleware added.

Router -> ((Request -> Response) -> (Request -> Response)) -> Router

wrap-logging = fn(handler) =>
  fn(req) =>
    println "Request: ${req.path}"
    response = handler req
    println "Response: ${response.status}"
    response
router = add-middleware router wrap-logging

make-csrf-filter

Create a CSRF protection filter using Sec-Fetch-Site header validation.

Uses the Sec-Fetch-Site header to validate that state-changing requests (POST, PUT, DELETE, PATCH) come from same-origin or same-site sources.

Parameters:

  • options - Options record with optional exclude-paths field
  • {exclude-paths - ["/webhook", "/api/public"]}

Returns: Filter function that validates CSRF for state-changing requests.

{exclude-paths: [String]} -> (Request -> Request | Response)

csrf-filter = make-csrf-filter {exclude-paths: ["/webhook"]}
router = add-before-filter router csrf-filter

simple-csrf-filter

Simple CSRF protection filter with no exclusions.

Returns: Filter function that validates CSRF for all state-changing requests.

Request -> Request | Response

router = add-before-filter router simple-csrf-filter

http-get

Register a GET route handler.

Parameters:

  • router - Router record
  • pattern - URL pattern
  • handler - Handler function

Returns: Updated Router

Router -> String -> (Request -> Response) -> Router

http-post

Register a POST route handler.

Parameters:

  • router - Router record
  • pattern - URL pattern
  • handler - Handler function

Returns: Updated Router

Router -> String -> (Request -> Response) -> Router

http-put

Register a PUT route handler.

Parameters:

  • router - Router record
  • pattern - URL pattern
  • handler - Handler function

Returns: Updated Router

Router -> String -> (Request -> Response) -> Router

http-delete

Register a DELETE route handler.

Parameters:

  • router - Router record
  • pattern - URL pattern
  • handler - Handler function

Returns: Updated Router

Router -> String -> (Request -> Response) -> Router

http-patch

Register a PATCH route handler.

Parameters:

  • router - Router record
  • pattern - URL pattern
  • handler - Handler function

Returns: Updated Router

Router -> String -> (Request -> Response) -> Router

http-head

Register a HEAD route handler.

Parameters:

  • router - Router record
  • pattern - URL pattern
  • handler - Handler function

Returns: Updated Router

Router -> String -> (Request -> Response) -> Router

http-options

Register an OPTIONS route handler.

Parameters:

  • router - Router record
  • pattern - URL pattern
  • handler - Handler function

Returns: Updated Router

Router -> String -> (Request -> Response) -> Router

find-route

Find a matching route for the given method and path.

Searches the router's routes for a match with the given method and path. Returns the matching route and extracted path parameters if found.

Parameters:

  • router - Router record
  • method - HTTP method to match
  • path - URL path to match

Returns: Some (route, params) if match found, None otherwise.params is a Map of parameter names to values.

Router -> String -> String -> Option (Route, Map String String)

match find-route router "GET" "/users/123"
  | Some (route, params) ->
    # params contains {id: "123"} for pattern "/users/:id"
  | None -> # No route found

get-error-handler

Get a custom error handler for a status code if registered.

Parameters:

  • router - Router record
  • status-code - HTTP status code

Returns: Some handler if custom handler registered, None otherwise

Router -> Int -> Option (Request -> Response)

default-error-response

Create a default error response for the given status code.

Parameters:

  • status-code - HTTP status code

Returns: Plain text error response with standard message for the status code.

Int -> Response

resp = default-error-response 404  # "Not Found"

handle-error

Handle an error with optional custom error handler.

Calls custom error handler if registered, otherwise returns default error response.

Parameters:

  • router - Router record
  • request - Request record
  • status-code - HTTP status code to handle

Returns: Error response from custom handler or default error response.

Router -> Request -> Int -> Response

resp = handle-error router req 404

route-request

Route a request through the router and return a response.

Processes a request through the full routing pipeline: 1. Applies Ring-style middleware (outermost first) 2. Runs before filters (can short-circuit) 3. Matches route and calls handler (or serves static file) 4. Runs after filters on response

Parameters:

  • router - Router record
  • request - Request record

Returns: Response record from handler or error handler.

Router -> Request -> Response

response = route-request router request

make-handler

Create a handler function from the router for composition or testing.

Creates a standalone handler function that can be composed with other middleware or used for testing without starting a server.

Parameters:

  • router - Router record

Returns: Handler function (request -> response) with all middleware applied.

Router -> (Request -> Response)

handler = make-handler router
response = handler request