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
Installation
chicken-install schematra
SRFI-1 Compatibility: If you're using SRFI-1 and need its delete function alongside Schematra's HTTP DELETE verb, rename one on import:
;; 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
Basic Usage
(import schematra chiccup) ;; Define routes (get ("/") (ccup->html `[h1 "Hello, Schematra!"])) (post ("/submit") "Form submitted!") ;; Start server (schematra-install) (schematra-start)
API
Route Definition
[syntax] (get (route) body ...)[syntax] (post (route) body ...)
[syntax] (put (route) body ...)
[syntax] (delete (route) body ...)
Define HTTP route handlers for the given path. Route can contain URL parameters using :param syntax.
;; 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)])))[parameter] (current-params)
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.
;; 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"[procedure] (halt status [body] [headers])
Immediately stop request processing and send an HTTP response.
(halt 'not-found "Page not found") (halt 'bad-request "{\"error\": \"Invalid input\"}" '((content-type application/json)))[procedure] (redirect location [status])
Redirect the client to a different URL. Default status is found (302).
(redirect "/login") (redirect "/new-location" 'moved-permanently)[procedure] (send-json-response datum [status])
Send a JSON response with proper content-type headers. Default status is ok (200).
(send-json-response '((status . "healthy") (version . "1.0"))) (send-json-response '((error . "Not found")) 'not-found)[procedure] (static path directory)
Serve static files from directory at URL path prefix.
[procedure] (schematra-install)Install the schematra route handlers.
[procedure] (schematra-start #!key development? nrepl?)Start the web server.
- development?: Enable development mode (default: #f)
- nrepl?: Start NREPL server in dev mode (default: #t)
Response Format
Route handlers can return:
String Response: Returns 200 OK with the string as body
(get ("/hello") "Hello, World!")
Response Tuple: Format (status body [headers]) for full control
'(created "User created successfully") '(ok "{\"message\": \"success\"}" ((content-type application/json)))
Chiccup Rendering
[procedure] (ccup->html s-html)[procedure] (ccup->sxml s-html)
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.
;; 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>")]
Middleware System
[procedure] (use-middleware! middleware-function)Install middleware functions that process requests before route handlers.
Middleware signature:
(define (my-middleware next)
;; Process request before handler
(let ((response (next))) ; Call next middleware or handler
;; Process response after handler
response))
Middleware can access and modify request parameters using (current-params) and (current-params new-params).
Session Middleware
(import sessions) (use-middleware! (session-middleware "secret-key")) ;; Session functions (session-get "key" [default]) (session-set! "key" value) (session-delete! "key") (session-destroy!)
Body Parser Middleware
(import schematra-body-parser) (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... ))
OAuth2 Authentication (Oauthtoothy)
Oauthtoothy provides complete OAuth2 authentication integration.
[procedure] (oauthtoothy-middleware providers #!key success-redirect save-proc load-proc)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)
Base URL for OAuth2 callback URLs.
[parameter] (current-auth)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:
(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)))
Complete OAuth2 Example
(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)))) ;; 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)
Examples
Simple Web App with Sessions
(import schematra chiccup sessions) (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)
JSON API
(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))))
License
Copyright © 2025 Rolando Abarca. Released under the GNU General Public License v3.0.