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

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:

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.

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="{&quot;theme&quot;:&quot;dark&quot;}">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:

[parameter] (auth-base-url [url])

Base URL for OAuth2 callback URLs.

[parameter] (current-auth)

Current user's authentication state. Returns association list with:

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.

Repository & full docs

GitHub Repository

Full docs