Schematra Logo

(Schematra)

Write web apps the way you think. Express HTML as data with Chiccup. Build components that compose naturally. Create powerful middleware with simple functions. Authentication in 3 lines, not 30.

Zero Config Sessions

Cookie-based sessions work immediately. No setup, no database, no complexity. session-set! and you're done.

Chiccup: HTML as Data

No more template syntax headaches. Write `[.card [h1 "Title"]] and get clean HTML. Map over lists, compose functions, build UIs that make sense.

3-Line Middleware

Real middleware that composes. Write a function, call use-middleware!, done. No decorators, no magic, just functions.

Why Developers Choose Schematra

πŸš€ Write Less, Build More

Complete auth flows in 20 lines. Middleware in 3 lines. Components that compose naturally with Chiccup. Zero boilerplate, maximum clarity.

⚑ Chiccup Magic

Your HTML structure is your data structure. No template engines, no context switching, no surprises. Just pure functional UI composition.

🎯 Functions All The Way

Middleware is just (lambda (next) ...). Routes are functions. Components are functions. Simple, composable, testable.

πŸ”§ Deploy Anywhere

Compile to a single binary. No runtime dependencies, no complex deployments. If it runs C, it runs Schematra.

Latest from the Blog

2026-05-30

What's New in Schematra 0.7

WebSocket support lands in Schematra. Real-time, bidirectional routes alongside your HTTP handlers, plus a modular reorganization that splits framework helpers into focused submodules.

releasewebsocketsreal-time
2026-05-08

What's New in Schematra 0.6.8

Structured JSON access logs, cleaner production observability, and a recap of the 0.6.x fixes that landed after the previous release post.

releaseloggingobservability

Ready to write web apps that make sense?

Getting Started

Installation

Option 1: Install with CHICKEN

chicken-install schematra

Option 2: Try with Docker

docker run --rm -it ghcr.io/schematra/schematra:latest csi

Then create your first app by creating a new file (e.g., app.scm) and start coding!

Experience Chiccup

See how HTML-as-data transforms the way you build components

Chiccup Code

Live Preview

Click 'Render Preview' to see the output

✨ Live Chiccup rendering! Edit the code above and watch HTML structure mirror your data structure in real-time.

See More Examples

Chiccup Components

;; Chiccup: HTML that looks like your data
(define (render-todo todo)
  `[.todo-item.p-4.border.rounded
    [h3.font-bold ,(todo-title todo)]
    [p.text-gray-600 ,(todo-description todo)]
    [.flex.gap-2.mt-2
     [button.bg-green-500.text-white.px-3.py-1.rounded
      (@ (onclick ,(format "completeTodo(~a)" (todo-id todo))))
      "Complete"]
     [button.bg-red-500.text-white.px-3.py-1.rounded
      (@ (onclick ,(format "deleteTodo(~a)" (todo-id todo))))
      "Delete"]]])

(get "/todos"
     (let ((todos (get-user-todos (session-get "user-id"))))
       (ccup->html
        `[.container.mx-auto.p-6
          [h1.text-2xl.mb-4 "My Todos"]
          ,@(map render-todo todos)])))

Build dynamic UIs with pure functions. Map over data, compose components, and create interactive interfaces that feel natural.

Simple Middleware

;; Powerful middleware for cross-cutting concerns
(define (auth-middleware next)
  (let ((token (cdr (assoc 'token (current-params)))))
    (if (and token (valid-token? token))
        (next)  ; Continue to route handler
        '(unauthorized "Invalid token"))))

(define (logging-middleware next)
  (let* ((request (current-request))
         (method (request-method request))
         (path (uri-path (request-uri request))))
    (log-dbg "~A ~A" method path)
    (next)))

(use-middleware! logging-middleware)
(use-middleware! auth-middleware)

;; Now all routes are logged and require auth
(get "/api/users"
     '(ok "{\"users\": [...]}" 
          ((content-type application/json))))

Compose powerful middleware for logging, authentication, and more. Each middleware is just a simple function.

Complete Web App

;; Complete web app in just a few lines
(import schematra chiccup sessions)

(define app (schematra/make-app))
(with-schematra-app app
 (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) equal?)))
         (session-set! "username" username)
         (redirect "/")))

 (schematra-install)
 (schematra-start))

A full authentication flow with sessions, forms, and redirects. Notice how natural HTML generation feels with Chiccup.

JSON APIs Made Easy

;; JSON APIs made effortless
(post "/api/users"
      (let* ((params (current-params))
             (name (alist-ref "name" params equal?))
             (email (alist-ref "email" params equal?)))
        (if (and name email (valid-email? email))
            (let ((user-id (create-user! name email)))
              (send-json-response
                'created
                `((id . ,user-id)
                  (message . "User created")
                  (email . ,email))))
            (send-json-response
              'bad-request
              '((error . "Invalid name or email")
                (required . ("name" "email")))))))

(get "/api/users"
     (let ((users (get-all-users)))
       (send-json-response 
         'ok 
         `((users . ,(map user->alist users))
           (count . ,(length users))))))

Write APIs that work with data, not strings. send-json-response handles serialization and headers automatically.

Testing Without a Server

;; Testing routes without a server - fast and isolated!
(import test schematra schematra.test srfi-13 chicken.format medea)

;; Create isolated test app
(define test-app (schematra/make-app))

;; Define routes in test app - using chiccup responses
(with-schematra-app test-app
  (lambda ()
    ;; You can (import routes) or (include-relative "path/to/routes.scm") here
    ;; Adding some explicitly for educational purposes
    (get "/hello" '(ccup [h1 "Hello, World!"]))

    (get "/users/:id"
         (let ((id (alist-ref "id" (current-params) equal?)))
           `(ccup [div.user
                   [h2 ,(format "User ~a" id)]
                   [p "Profile page"]])))

    (post "/api/echo"
          (let ((name (alist-ref 'name (current-params))))
            (send-json-response `((message . ,(format "Hello ~a" name))))))

    ;; Add middleware that transforms responses (for demonstration)
    (use-middleware!
      (lambda (next)
        (let ((result (next)))
          ;; Middleware can inspect and transform responses
          (if (and (list? result) (eq? (car result) 'ccup))
              ;; Wrap chiccup responses with additional markup
              `(ccup [div.wrapped ,(cadr result)])
              result))))))

;; Run tests with the test egg
(test-group "Schematra Routes"

  (test "GET /hello returns response tuple with chiccup"
    '(ok (ccup [div.wrapped [h1 "Hello, World!"]]) ())
    (test-route test-app 'GET "/hello"))

  (test "GET /users/:id extracts params and wraps with middleware"
    '(ok (ccup [div.wrapped [div.user [h2 "User 123"] [p "Profile page"]]]) ())
    (test-route test-app 'GET "/users/123"))

  (test "Can extract just the chiccup body from response tuple"
    '(ccup [div.wrapped [h1 "Hello, World!"]])
    (test-route-body test-app 'GET "/hello"))

  (test "POST /api/echo returns JSON with correct content"
    '((message . "Hello Alice"))
    (read-json (test-route-body test-app 'POST "/api/echo?name=Alice")))

  (test "404 on unknown route"
    #f
    (test-route test-app 'GET "/unknown")))

;; Run: csi -s test-routes.scm
;; Output:
;; -- testing Schematra Routes --------------------------------------------------
;; GET /hello returns response tuple with chiccup ....................... [ PASS]
;; GET /users/:id extracts params and wraps with middleware ............. [ PASS]
;; Can extract just the chiccup body from response tuple ................ [ PASS]
;; POST /api/echo returns JSON with correct content ..................... [ PASS]
;; 404 on unknown route ................................................. [ PASS]
;; 5 tests completed in 0.001 seconds.
;; 5 out of 5 (100%) tests passed.
;; -- done testing Schematra Routes ---------------------------------------------
;;
;; Benefits:
;; βœ“ No HTTP server - tests run in milliseconds
;; βœ“ Test against response tuples: (status body headers)
;; βœ“ Assert on chiccup structure, not HTML strings
;; βœ“ Middleware can inspect and modify response tuples
;; βœ“ Complete isolation - each test gets its own app
;; βœ“ Verify params, routing, status codes, and response bodies
;; βœ“ Can still test rendered HTML with (current-body)

Test your routes in milliseconds with isolated app instances. No HTTP server neededβ€”just pure, fast unit tests.

OAuth2 Authentication

;; Provider configuration
(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)))

;; 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")))))

;; 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 "/"))

Add Google OAuth2 login to your app with oauthtoothy. Complete social authentication in under 20 lines.

Webhook Signature Verification

;; Webhook handler with HMAC-SHA256 signature verification
;; body-parser-middleware captures a replayable request body so you can verify it
(import schematra schematra.body-parser hmac sha2 message-digest)

(use-middleware! (body-parser-middleware))

(define (hmac-sha256-hex key data)
  (message-digest->string
   (hmac-message-digest (open-input-string key)
                        sha256-primitive
                        (open-input-string data) byte)))

(define (signature-valid? secret raw sig)
  (and sig
       (string=? sig (string-append "sha256="
                                    (hmac-sha256-hex secret raw)))))

(post "/webhook"
  (let* ((raw (request-body-string (current-request-body)))
         (sig (alist-ref "x-hub-signature-256"
                          (request-headers (current-request)) equal?)))
    (if (signature-valid? (get-environment-variable "WEBHOOK_SECRET") raw sig)
        (begin
          (process-event! (read-json raw))
          '(ok "Received"))
        '(forbidden "Invalid signature"))))

;; Test without a live server β€” body: is captured by body-parser-middleware
(define secret "test-secret")
(define payload "{\"action\":\"push\"}")
(define valid-sig (string-append "sha256=" (hmac-sha256-hex secret payload)))

(test "rejects request with missing signature"
  'forbidden
  (test-route-status webhook-app 'POST "/webhook"
                     body: payload))

(test "accepts request with valid signature"
  'ok
  (test-route-status webhook-app 'POST "/webhook"
                     body: payload
                     headers: `((x-hub-signature-256 . ,valid-sig))))

Verify webhook payloads from GitHub, Stripe, or any HMAC-SHA256 provider. body-parser-middleware captures a replayable request body so your signature check sees exactly what was sent.