Wiki
Download
Manual
Eggs
API
Tests
Bugs
show
edit
history
You can edit this page using
wiki syntax
for markup.
Article contents:
== Schematra A modern web framework for CHICKEN Scheme inspired by Sinatra that combines simplicity with power. === Description Schematra is a lightweight, expressive web framework for CHICKEN Scheme built on top of Spiffy that brings the elegance of Lisp to web development. It features minimal boilerplate, Chiccup HTML rendering, flexible routing, built-in sessions, and extensible middleware support. === Requirements * CHICKEN Scheme 5.0 or later * Dependencies: spiffy, format, openssl, message-digest, hmac, sha2, http-client, medea, nrepl, logger * WebSocket support ({{schematra.ws}}) additionally uses base64 and simple-sha1 === Installation <enscript highlight="bash"> chicken-install schematra </enscript> '''SRFI-1 Compatibility:''' If you're using SRFI-1 and need its {{delete}} function alongside Schematra's HTTP DELETE verb, rename one on import: <enscript highlight="scheme"> ;; Rename SRFI-1's delete (recommended) (import (rename srfi-1 (delete srfi1:delete)) schematra chiccup) (delete "/users/:id" ...) ; HTTP DELETE route (srfi1:delete 'x my-list) ; Remove items from list </enscript> === Basic Usage <enscript highlight="scheme"> (import schematra chiccup) ;; Create app instance (define app (schematra/make-app)) ;; Define routes within app context (with-schematra-app app (lambda () (get "/" (ccup->html `[h1 "Hello, Schematra!"])) (post "/submit" "Form submitted!") ;; Install and start server (schematra-install) (schematra-start))) </enscript> === API ==== Features Based on the value of {{SCHEMATRA_ENV}}, one of two features is registered: * {{schematra-dev}}: if the value of {{SCHEMATRA_ENV}} is equal to the string "development". * {{schematra-prod}}: if the value of {{SCHEMATRA_ENV}} is anything else. <enscript highlight="scheme"> (cond-expand (schematra-prod (logger/format 'json)) ;; use JSON logs in production (else (logger/level 'debug))) ;; verbose logging in development </enscript> ==== App Management <procedure>(schematra/make-app [config])</procedure> Creates a new isolated Schematra application instance. Each app has its own routes and middleware. <enscript highlight="scheme"> (define app (schematra/make-app)) </enscript> <syntax>(with-schematra-app app thunk)</syntax> Sets the current app context for route and middleware definitions. The thunk must be a zero-argument procedure. <enscript highlight="scheme"> (with-schematra-app app (lambda () (get "/" "Hello!") (use-middleware! my-middleware) (schematra-install) (schematra-start))) </enscript> <parameter>(current-app [app])</parameter> Get or set the current application instance. Used internally by route macros and middleware functions. ==== Route Definition <syntax>(get route body ...)</syntax> <syntax>(post route body ...)</syntax> <syntax>(put route body ...)</syntax> <syntax>(delete route body ...)</syntax> Define HTTP route handlers for the given path. Routes must be defined within a {{with-schematra-app}} context. Route can contain URL parameters using {{:param}} syntax. <enscript highlight="scheme"> (with-schematra-app app (lambda () ;; Simple routes (get "/" "Home page") (get "/about" (ccup->html `[h1 "About Us"])) ;; URL parameters (get "/user/:id" (let ((id (alist-ref "id" (current-params) equal?))) (ccup->html `[h1 ,(string-append "User " id)]))))) </enscript> <parameter>(current-params)</parameter> Returns an association list containing the current request parameters: * '''Path parameters''' (string keys): URL segments starting with ':' * '''Query parameters''' (symbol keys): URL query string parameters * '''Form data''' (symbol keys): POST form fields (requires body-parser-middleware) This parameter only contains meaningful data inside a route. Outside it will default to {{#f}}. <enscript highlight="scheme"> ;; For route /users/:id and request /users/123?format=json (current-params) ; => '(("id" . "123") (format . "json")) ;; Access path parameter (alist-ref "id" (current-params) equal?) ; => "123" ;; Access query parameter (alist-ref 'format (current-params)) ; => "json" </enscript> <procedure>(halt status [body] [headers])</procedure> Immediately stop request processing and send an HTTP response. <enscript highlight="scheme"> (halt 'not-found "Page not found") (halt 'bad-request "{\"error\": \"Invalid input\"}" '((content-type application/json))) </enscript> <procedure>(redirect location [status])</procedure> Redirect the client to a different URL. Default status is {{found}} (302). <enscript highlight="scheme"> (redirect "/login") (redirect "/new-location" 'moved-permanently) </enscript> <procedure>(send-json-response datum [status])</procedure> Send a JSON response with proper content-type headers. Default status is {{ok}} (200). <enscript highlight="scheme"> (send-json-response '((status . "healthy") (version . "1.0"))) (send-json-response '((error . "Not found")) 'not-found) </enscript> <procedure>(static path directory)</procedure> Serve static files from {{directory}} at URL {{path}} prefix. <procedure>(schematra-install)</procedure> Install the schematra route handlers. <procedure>(schematra-start #!key (port 8080) (bind-address #f) (log-output (current-output-port)) (log-level 'info) (log-format 'text))</procedure> Start the web server. Logging is handled by the [[logger]] egg. * {{port:}} the port to listen to, defaults to 8080 * {{bind-address:}} whether or not to bind to a specific address. If #f then it binds to 0.0.0.0 * {{log-output:}} output port for logs. Defaults to {{(current-output-port)}}. * {{log-level:}} minimum log level — {{'debug}}, {{'info}}, {{'warn}}, or {{'error}}. Defaults to {{'info}}. * {{log-format:}} output format — {{'text}} or {{'json}}. Defaults to {{'text}}. When {{log-format}} is {{'json}}, access logs are emitted as structured logger entries with message {{"request"}} and fields such as {{remote_addr}}, {{method}}, {{uri}}, {{response_code}}, {{referer}}, and {{user_agent}}. Text logs keep the default Spiffy-style access log line. When {{SCHEMATRA_ENV}} is set to {{"development"}}, the server starts on a separate thread. ==== Cookies <procedure>(cookie-set! key val #!key (path "/") (max-age #f) (secure #f) (http-only #f) (domain #f))</procedure> Set a cookie in the HTTP response. Must be called within a route handler. <enscript highlight="scheme"> ;; Simple session cookie (cookie-set! "user_id" "12345") ;; Persistent cookie with 1 day expiration (cookie-set! "preferences" "theme=dark" max-age: "86400") ;; Secure authentication cookie (cookie-set! "auth_token" token-value secure: #t http-only: #t max-age: "3600") </enscript> <procedure>(cookie-ref key [default])</procedure> Read a cookie value from the current request. Returns {{default}} (or {{#f}}) if the cookie is not present. <enscript highlight="scheme"> (let ((user-id (cookie-ref "user_id"))) (if user-id (format "Welcome, user ~A" user-id) "Please log in")) </enscript> <procedure>(cookie-delete! key #!key (path "/"))</procedure> Delete a cookie by setting its max-age to 0. <enscript highlight="scheme"> (cookie-delete! "user_id") </enscript> ==== Server-Sent Events <procedure>(sse path handler)</procedure> Register a Server-Sent Events endpoint. The handler receives the request and should loop to send events using {{write-sse-data}}. SSE headers (Content-Type, Cache-Control, Connection) are set automatically. <procedure>(write-sse-data data #!key id event)</procedure> Send an SSE message to the connected client. * {{data}}: string - The message data * {{id}}: string or #f - Optional event ID for client-side tracking * {{event}}: string or #f - Optional event type name <enscript highlight="scheme"> ;; Simple time server (sse ("/time" req) (let loop () (write-sse-data (current-time-string) event: "time-update") (thread-sleep! 1) (loop))) </enscript> Client-side usage: <enscript highlight="javascript"> const es = new EventSource('/time'); es.addEventListener('time-update', function(e) { console.log('Time:', e.data); }); </enscript> ==== WebSockets WebSockets give you a bidirectional, message-oriented channel between the server and the browser. Schematra implements RFC 6455 in the {{schematra.ws}} module — the WebSocket primitives are '''not''' part of the core {{schematra}} module, so you add a second import. If you don't use WebSockets, you don't pay for the extra dependencies ({{base64}}, {{simple-sha1}}, etc.). <enscript highlight="scheme"> (import schematra schematra.ws) (websocket "/echo" (on-open (send-text "welcome")) (on-text message (send-text (string-append "you said: " message))) (on-close code reason (void))) </enscript> The framework validates the upgrade request, performs the handshake, decodes each incoming frame, and dispatches to the matching clause. Your code only sees decoded messages. <syntax>(websocket path clause ...)</syntax> Register a WebSocket route at {{path}}. The route syntax is the same family as {{get}}/{{post}}/{{sse}}. Each clause matches an event; all are optional except {{on-text}}: * {{(on-open body ...)}} — runs once when the handshake completes. * {{(on-text message body ...)}} — runs for each text frame. {{message}} is bound to the decoded string payload. * {{(on-binary bytes body ...)}} — runs for each binary frame. {{bytes}} is the raw byte string. * {{(on-close code reason body ...)}} — runs when the connection closes, regardless of who initiated it. {{code}} is the WebSocket close code (1000 for normal, 1006 for an abrupt disconnect, etc.) and {{reason}} is the human-readable string. * {{(on-error exn body ...)}} — runs if a handler raises an exception, before the connection is closed. Use it for logging, metrics, or notifying other clients. You can omit {{on-binary}}, {{on-error}}, or both, and there's a single-clause form for echo-style routes: <enscript highlight="scheme"> (websocket "/echo" (on-text message (send-text message))) </enscript> <syntax>(websocket* path on-open on-text on-binary on-close on-error)</syntax> Procedural form behind the {{websocket}} macro. All five handler thunks are required. Use this if you want to build a WebSocket route programmatically. <procedure>(send-text message)</procedure> Send a UTF-8 text frame on {{(current-websocket)}}. {{message}} is a string. Raises an error if there is no current WebSocket connection. <procedure>(send-binary bytes)</procedure> Send a binary frame on {{(current-websocket)}}. {{bytes}} is a byte string. Both implicitly target {{(current-websocket)}}, the connection bound by the current handler. Sends are mutex-guarded per connection, so it's safe to call them from multiple threads as long as you parameterize {{current-websocket}} to the connection you want to write to. <parameter>(current-websocket)</parameter> Inside a WebSocket handler, holds the current connection as a {{websocket-connection}} record. You can store it, share it across threads, and reach for it later — that's how broadcasting works. Parameterize it to redirect {{send-text}}/{{send-binary}} at another connection. <procedure>(close-websocket! [code [reason [ws]]])</procedure> Initiate a graceful close. * {{code}}: integer close code (default {{1000}} for normal closure). * {{reason}}: human-readable string (default {{""}}). * {{ws}}: the connection to close (default {{(current-websocket)}}). Sends the close frame to the peer (best effort) and tears down the underlying input port so the connection's read loop unblocks immediately — even when called from another thread. The connection's {{on-close}} clause still runs for cleanup. <enscript highlight="scheme"> ;; From inside a handler — close the current connection (code 1000): (on-text message (when (string=? message "/bye") (close-websocket!))) ;; From another thread — force-close a specific stored connection: (close-websocket! 1008 "policy violation" some-other-ws) </enscript> '''Broadcasting:''' because {{current-websocket}} is a first-class value, you can keep a registry of connections and write to each by parameterizing the connection. Always snapshot the registry under a lock before iterating, then release the lock — {{send-text}} can block on slow clients, and you don't want to hold the lock during I/O. <enscript highlight="scheme"> (define clients-mutex (make-mutex)) (define clients '()) (define (broadcast! message) (let ((snapshot (dynamic-wind (lambda () (mutex-lock! clients-mutex)) (lambda () clients) (lambda () (mutex-unlock! clients-mutex))))) (for-each (lambda (ws) (parameterize ((current-websocket ws)) (condition-case (send-text message) (exn () (set! clients (delete ws clients)))))) snapshot))) (websocket "/chat" (on-open (set! clients (cons (current-websocket) clients)) (broadcast! "a new client joined")) (on-text message (broadcast! message)) (on-close code reason (set! clients (delete (current-websocket) clients)) (broadcast! "a client left"))) </enscript> '''Error handling:''' if a handler raises an exception, Schematra runs your {{on-error}} clause first, then sends a {{1011}} close frame, runs {{on-close}}, and lets the framework log the original exception. The connection is always closed cleanly, so you don't need to wrap {{send-text}} in {{condition-case}} for normal usage. '''Configuration:''' three parameters bound incoming traffic, all in {{schematra.ws}}: <parameter>(websocket-max-frame-size [bytes])</parameter> Maximum size of a single incoming frame. Default 1 MB. Larger frames are rejected before the payload is read (a defense against buffer-exhaustion attacks) and the connection is closed with code {{1009}} (message too big). <parameter>(websocket-max-message-size [bytes])</parameter> Maximum total bytes for a fragmented message (sum of all fragments). Default 16 MB. Must be {{>=}} {{websocket-max-frame-size}}. Exceeding it closes the connection with code {{1009}}. <parameter>(websocket-max-fragment-count [n])</parameter> Maximum number of fragments per message. Default 1024. Prevents a peer from sending millions of tiny fragments that stay under the message-size cap but exhaust memory. Exceeding it closes with code {{1009}}. <enscript highlight="scheme"> (websocket-max-frame-size (* 4 1024 1024)) ;; 4 MB </enscript> '''RFC 6455 conformance:''' the implementation is run against the [[https://github.com/crossbario/autobahn-testsuite|Autobahn TestSuite]] on every release, with a strict pass of 296/301 (98.3%) across framing, fragmentation, control frames, UTF-8, close handling, and limits. The remaining cases are RFC-undefined or INFORMATIONAL behavior; {{permessage-deflate}} compression is not implemented. A complete multi-client chat lives in {{examples/websocket-demo.scm}}. ==== Response Format Route handlers can return: '''String Response:''' Returns 200 OK with the string as body <enscript highlight="scheme"> (get "/hello" "Hello, World!") </enscript> '''Response Tuple:''' Format {{(status body [headers])}} for full control <enscript highlight="scheme"> '(created "User created successfully") '(ok "{\"message\": \"success\"}" ((content-type application/json))) </enscript> '''chiccup lists:''' Return a list that begins with the symbol 'ccup. The Schematra router will automatically pass the cdr of that list to {{ccup->html}}. It's useful to return a chiccup list for testing and composition at the middleware level. <enscript highlight="scheme"> '(ccup [h1 "hello" ...]) </enscript> === Chiccup Rendering <procedure>(ccup->html s-html)</procedure> <procedure>(ccup->sxml s-html)</procedure> Convert Chiccup list to HTML string or SXML. Chiccup allows writing HTML using Lisp syntax with CSS class integration. Behind the scenes, chiccup converts the simplified chiccup-style lists to SXML and then to HTML using sxml-transforms. You can use {{ccup->sxml}} to get the intermediate SXML representation and leverage the full power of the sxml-transforms ecosystem for advanced transformations. <enscript highlight="scheme"> ;; Basic elements `[h1 "Hello World"] ;; CSS classes and IDs - default element is "div" ;; Note: ID (#id) must come after classes `[.container.mx-auto#main "Content"] ;; => <div class="container mx-auto" id="main">Content</div> ;; Attributes with @ syntax `[a (@ (href "/page")) "Link"] `[input (@ (type "text") (name "username"))] ;; Attribute values are automatically HTML-escaped (like React/Vue) `[div (@ (data-config "{\"theme\":\"dark\"}")) "Content"] ;; => <div data-config="{"theme":"dark"}">Content</div> ;; Boolean attributes (no values) `[input (@ (type "checkbox") (checked) (disabled))] ;; => <input type="checkbox" checked disabled> ;; Conditional attributes (let ((is-disabled #t)) `[button (@ (type "submit") ,@(if is-disabled '((disabled)) '())) "Submit"]) ;; => <button type="submit" disabled>Submit</button> ;; Dynamic content `[ul ,@(map (lambda (item) `[li ,item]) items)] ;; Raw HTML (unescaped) `[div (raw "<em>emphasized</em>")] </enscript> See [[chiccup]] for more details. === Middleware System <procedure>(use-middleware! middleware-function)</procedure> Install middleware functions that process requests before route handlers. Must be called within a {{with-schematra-app}} context. Middleware signature: <enscript highlight="scheme"> (define (my-middleware next) ;; Process request before handler (let ((response (next))) ; Call next middleware or handler ;; Process response after handler response)) </enscript> Middleware can access and modify request parameters using {{(current-params)}} and {{(current-params new-params)}}. ==== Session Middleware <enscript highlight="scheme"> (import schematra-session) (with-schematra-app app (lambda () (use-middleware! (session-middleware "secret-key")) ;; Session functions available in route handlers: ;; (session-get "key" [default]) ;; (session-set! "key" value) ;; (session-delete! "key") ;; (session-destroy!) (get "/profile" (let ((user (session-get "username"))) (if user (format "Welcome, ~A!" user) (redirect "/login")))))) </enscript> See [[schematra-session]] for full documentation. ==== Body Parser Middleware The body parser captures request bodies into {{(current-request-body)}}. For {{application/x-www-form-urlencoded}} requests, it also parses fields into {{(current-params)}} using symbol keys. Bodies larger than {{(request-body-spool-threshold)}} are spooled to a temporary file, while callers still use the same replayable body API. <enscript highlight="scheme"> (import schematra.body-parser) (with-schematra-app app (lambda () (use-middleware! (body-parser-middleware)) ;; Automatically parses form data and adds to (current-params) ;; Form fields become symbol keys (post "/login" (let ((username (alist-ref 'username (current-params)))) ;; Handle login... )))) </enscript> <parameter>(current-request-body)</parameter> Returns a replayable request body object, or {{#f}} if there was no body. Use {{(request-body-port body)}} to get a fresh input port, or {{(request-body-string body)}} when you explicitly need the entire body as a string. <procedure>(request-body-port body)</procedure> Returns a fresh input port for the captured request body, regardless of whether the body is backed by an in-memory string or a temporary file. <procedure>(request-body-string body)</procedure> Returns the captured request body as a string. This may load the full body into memory when the body was spooled to disk. <parameter>(request-body-spool-threshold [bytes])</parameter> Controls when captured request bodies are spooled to a temporary file. The default is 5MB. ===== Multipart Forms {{body-parser-middleware}} captures {{multipart/form-data}} request bodies, but it does not automatically merge multipart fields into {{(current-params)}}. Decode multipart bodies explicitly with {{read-current-multipart-form-data}}. <procedure>(read-current-multipart-form-data [max-length])</procedure> Decodes the current multipart request body using the body captured by {{body-parser-middleware}}. It opens a fresh {{request-body-port}} and calls {{multipart-form-data-decode}}, so it does not try to re-read the original request port. Do not call {{read-multipart-form-data}} from the [[multipart-form-data]] egg after installing {{body-parser-middleware}}; the original request port has already been consumed. <enscript highlight="scheme"> (import schematra.body-parser multipart-form-data) (with-schematra-app app (lambda () (use-middleware! (body-parser-middleware)) (post "/upload" (let* ((parts (read-current-multipart-form-data)) (title (alist-ref 'title parts)) (upload (alist-ref 'file parts))) (when (multipart-file? upload) (let ((filename (multipart-file-filename upload)) (port (multipart-file-port upload))) (save-upload! filename port))) (string-append "Uploaded: " title))))) </enscript> === OAuth2 Authentication (Oauthtoothy) Oauthtoothy provides complete OAuth2 authentication integration. <procedure>(oauthtoothy-middleware providers #!key success-redirect save-proc load-proc)</procedure> Creates OAuth2 authentication middleware. '''Parameters:''' * {{providers}}: List of OAuth2 provider configurations * {{success-redirect}}: URL to redirect after successful auth (default: "/") * {{save-proc}}: Function to save user data (optional) * {{load-proc}}: Function to load user data (optional) <parameter>(auth-base-url [url])</parameter> Base URL for OAuth2 callback URLs. <parameter>(current-auth)</parameter> Current user's authentication state. Returns association list with: * {{authenticated?}}: Boolean indicating if user is logged in * {{user-id}}: Unique identifier for the user * Additional user data from provider ==== Provider Configuration Each provider is an association list with required keys: <enscript highlight="scheme"> (define (google-provider #!key client-id client-secret) `((name . "google") (client-id . ,client-id) (client-secret . ,client-secret) (auth-url . "https://accounts.google.com/o/oauth2/auth") (token-url . "https://oauth2.googleapis.com/token") (user-info-url . "https://www.googleapis.com/oauth2/v2/userinfo") (scopes . "profile email") (user-info-parser . ,parse-google-user))) </enscript> ==== Complete OAuth2 Example <enscript highlight="scheme"> (import schematra schematra-session oauthtoothy chiccup) ;; User data parser (define (parse-google-user json-response) `((id . ,(alist-ref 'id json-response)) (name . ,(alist-ref 'name json-response)) (email . ,(alist-ref 'email json-response)))) ;; Create app (define app (schematra/make-app)) (with-schematra-app app (lambda () ;; Install middleware (use-middleware! (session-middleware "secret-key")) (use-middleware! (oauthtoothy-middleware (list (google-provider client-id: (get-environment-variable "GOOGLE_CLIENT_ID") client-secret: (get-environment-variable "GOOGLE_CLIENT_SECRET"))) success-redirect: "/profile")) ;; Protected route (get "/profile" (let ((auth (current-auth))) (if (alist-ref 'authenticated? auth) (ccup->html `[h1 ,(string-append "Welcome, " (alist-ref 'name auth))]) (redirect "/auth/google")))) ;; Logout (get "/logout" (session-destroy!) (redirect "/")) (schematra-install) (schematra-start))) </enscript> See [[oauthtoothy]] for full documentation. === Testing Schematra includes a built-in testing module ({{schematra.test}}) for testing routes, middleware, cookies, and sessions without starting a server. ==== Setup <enscript highlight="bash"> chicken-install test </enscript> <enscript highlight="scheme"> (import schematra schematra.test test) </enscript> ==== Creating a Test App Use {{with-test-app}} to create an app with routes: <enscript highlight="scheme"> (define app (with-test-app a (get "/" "Hello World") (get "/users/:id" (let ((id (alist-ref "id" (current-params) equal?))) (string-append "User " id))))) </enscript> The variable name ({{a}} above) is bound to the app inside the body, so you can use it for middleware setup: <enscript highlight="scheme"> (define app (with-test-app a (use-middleware! (session-middleware "secret")) (get "/profile" (session-get "user" "anonymous")))) </enscript> ==== Route Testing Helpers <procedure>(test-route app method path #!key headers body cookies)</procedure> Test a route and return the response tuple {{(status body headers)}} or {{#f}} when no route matches. <procedure>(test-route-status app method path #!key headers body cookies)</procedure> Return only the response status symbol (e.g., {{'ok}}, {{'not-found}}) or {{#f}}. <procedure>(test-route-body app method path #!key headers body cookies)</procedure> Return only the response body string or {{#f}}. <procedure>(test-route-headers app method path #!key headers body cookies)</procedure> Return only the response headers alist or {{#f}}. <procedure>(test-route-full app method path #!key headers body cookies)</procedure> Return {{(status body headers cookies)}} including Set-Cookie response headers, or {{#f}}. <procedure>(test-route-cookies app method path #!key headers body cookies)</procedure> Return only the response cookies alist or {{#f}}. <procedure>(response-cookie-value full-result cookie-name)</procedure> Extract a specific cookie value from a {{test-route-full}} result. Returns the cookie value string or {{#f}}. <procedure>(test-route-redirect-location app method path #!key headers body cookies)</procedure> Return the Location header URI as a string or {{#f}}. All route-testing functions accept these keyword arguments: * {{headers:}} request headers alist (e.g., {{'((content-type application/json))}}) * {{body:}} request body string * {{cookies:}} request cookies alist (e.g., {{'(("name" . "value"))}}) ==== Test Examples <enscript highlight="scheme"> (import schematra schematra.test schematra-session test) ;; Basic routing tests (test-group "My routes" (let ((app (with-test-app a (get "/hello" "Hello World") (post "/echo" (alist-ref 'msg (current-params)))))) (test "GET /hello" 'ok (test-route-status app 'GET "/hello")) (test "GET /hello body" "Hello World" (test-route-body app 'GET "/hello")) (test "Unknown route" #f (test-route app 'GET "/unknown")))) ;; Cookie tests (test-group "Cookies" (let ((app (with-test-app a (get "/set" (begin (cookie-set! "k" "v") "done"))))) (test "Cookie is set" "v" (response-cookie-value (test-route-full app 'GET "/set") "k")))) ;; Session round-trip test (test-group "Sessions" (let* ((app (with-test-app a (use-middleware! (session-middleware "secret")) (get "/login" (session-set! "user" "alice") "ok") (get "/whoami" (session-get "user" "nobody")))) (full (test-route-full app 'GET "/login")) (cookie (response-cookie-value full (session-key)))) (test "Session persists" "alice" (test-route-body app 'GET "/whoami" cookies: (list (cons (session-key) cookie)))))) (test-exit) </enscript> Run tests with: <enscript highlight="bash"> csi -s tests/my_tests.scm </enscript> === Examples ==== Simple Web App with Sessions <enscript highlight="scheme"> (import schematra chiccup schematra-session) (define app (schematra/make-app)) (with-schematra-app app (lambda () (use-middleware! (session-middleware "secret-key")) (get "/" (let ((user (session-get "username"))) (if user (ccup->html `[h1 ,(format "Welcome back, ~a!" user)]) (redirect "/login")))) (get "/login" (ccup->html `[form (@ (method "POST") (action "/login")) [input (@ (type "text") (name "username") (placeholder "Username"))] [button "Login"]])) (post "/login" (let ((username (alist-ref 'username (current-params)))) (session-set! "username" username) (redirect "/"))) (schematra-install) (schematra-start))) </enscript> ==== JSON API <enscript highlight="scheme"> (define app (schematra/make-app)) (with-schematra-app app (lambda () (get "/api/users" (send-json-response `((users . ,(map user->alist (get-all-users))) (count . ,(length (get-all-users)))))) (post "/api/users" (let ((name (alist-ref 'name (current-params))) (email (alist-ref 'email (current-params)))) (if (and name email) (let ((user-id (create-user! name email))) (send-json-response `((id . ,user-id) (created . #t)) 'created)) (send-json-response '((error . "Invalid input")) 'bad-request)))) (schematra-install) (schematra-start))) </enscript> === License Copyright (c) 2025 Rolando Abarca. Released under the BSD-3-Clause License. === Repository & full docs [[https://github.com/schematra/schematra|GitHub Repository]] [[https://github.com/schematra/schematra/blob/main/docs/docs.md|Full docs]]
Description of your changes:
I would like to authenticate
Authentication
Username:
Password:
Spam control
What do you get when you add 8 to 7?