Schematra A minimal web framework for CHICKEN Scheme, inspired by Sinatra. Schematra is currently an early exploration project created for learning purposes, but hopefully it will grow into something more useful over time. Why Schematra? I created Schematra because I wanted to: Improve my knowledge of scheme : Building a web framework is a great way to explore a language's capabilities and idioms : Building a web framework is a great way to explore a language's capabilities and idioms Create something simple : Most web frameworks are complex beasts. Schematra aims to be minimal and understandable : Most web frameworks are complex beasts. Schematra aims to be minimal and understandable Enable modern web development: The framework is designed to work well with modern tools like Tailwind CSS and htmx, making it easy to build interactive web applications without heavy JavaScript frameworks. Although tbh this is completely agnostic to how the framework works, it's what most of my examples will use. Features Simple route definition with get and post functions and functions URL parameter extraction (e.g., /users/:id ) & body parsing ) & body parsing Middleware support Included naive session middleware (cookie storage) Development mode with REPL integration (leveraging emacs run-scheme ) ) Very simple hiccup inspired template system Built on top of the solid Spiffy web server Installation First, make sure you have CHICKEN Scheme installed. Once you have it installed, you can install schematra as an egg: git clone https://github.com/rolandoam/schematra cd schematra chicken-install That will download the dependencies and install the core modules so that they're available system-wide. Quick Start Here's a simple "Hello World" application: (import schematra) ; ; Define routes (get " / " ( lambda ( req params ) "Hello, World!")) (get " /users/:id " ( lambda ( req params ) ( let ((user-id (alist-ref " id " params equal?))) (format " User ID: ~A " user-id)))) (post " /submit " ( lambda ( req params ) ( let ((body (request-body-string req))) (format " Received: ~A " body)))) ; ; Start the server (schematra-install) (schematra-start port: 8080 ) Save this as app.scm and run: csi app.scm Visit http://localhost:8080 to see your application running. Development Mode For interactive development, start the server in development mode: (schematra-start development?: #t port: 8080 repl-port: 1234 ) This starts the web server in a background thread and opens an NREPL on port 1234. You can connect with your favorite Scheme editor or use nc localhost 1234 for a simple REPL session. For a more elegant environment, you can use emacs run-scheme by running C-u M-x run-scheme RET nc localhost 1234 . Route Parameters Schematra supports URL parameters using the :parameter syntax, as well as query params: ( define ( lookup key alist ) ( let ((pair ( assoc key alist))) ( if pair ( cdr pair) #f ))) (get " /users/:user-id/posts/:post-id " ( lambda ( req params ) ( let ((user-id (lookup " user-id " params)) (post-id (lookup " post-id " params)) (q (lookup 'q params))) ; ; query params use symbol keys (format " User: ~A, Post: ~A " user-id post-id)))) The params argument contains both URL parameters (with string keys) and query parameters (with symbol keys). Route Handlers Route handlers are functions that process HTTP requests and generate responses. Understanding how they work and what they should return is crucial for building Schematra applications. Handler Function Signature Every route handler must accept exactly two arguments: ( define ( my-handler request params ) ; ; Handler implementation ) request : The intarweb request object containing headers, method, URI, and request port : The intarweb request object containing headers, method, URI, and request port params : An association list containing both URL path parameters and query parameters The Request Object The request parameter provides access to all aspects of the HTTP request: (get " /debug " ( lambda ( req params ) ( let ((method (request-method req)) ; 'GET, 'POST, etc. (uri (request-uri req)) ; Full URI object (headers (request-headers req)) ; Request headers (port (request-port req))) ; Input port for body (format " Method: ~A, Path: ~A " method (uri-path uri))))) Common request operations: (request-method request) - Get HTTP method (GET, POST, etc.) - Get HTTP method (GET, POST, etc.) (request-uri request) - Get URI object with path, query, etc. (see uri-common) - Get URI object with path, query, etc. (see uri-common) (request-headers request) - Get request headers - Get request headers (request-body-string request) - Read request body as string (useful for POST data) The Params Argument The params argument is an association list containing two types of parameters: Path Parameters (string keys): Extracted from URL segments starting with : Query Parameters (symbol keys): Extracted from the URL query string ; ; Route: /users/:id?format=json&limit=10 ; ; URL: /users/123?format=json&limit=10 ; ; params = (("id" . "123") (format . "json") (limit . "10")) (get " /users/:id " ( lambda ( req params ) ( let ((user-id (alist-ref " id " params equal?)) ; Path param (string key) (format (alist-ref 'format params)) ; Query param (symbol key) (limit (alist-ref 'limit params))) ; Query param (symbol key) (format " User ~A, format: ~A, limit: ~A " user-id format limit)))) Handler Return Values Route handlers can return different types of values, which Schematra automatically converts to the corresponding intarweb response: 1. String Response (Most Common) Return a string to send a 200 OK response with that string as the body: (get " /hello " ( lambda ( req params ) "Hello, World!")) ; Returns 200 OK with "Hello, World!" body 2. Response List Return a list in the format (status body [headers]) for full control over the response: (get " /custom " ( lambda ( req params ) '(created " Resource created successfully " ))) ; Returns 201 Created (get " /with-headers " ( lambda ( req params ) '(ok " Success " ((content-type . " text/plain " ))))) ; With custom headers Some common valid status symbols include: ok (200) (200) created (201) (201) found (302) - for redirects (302) - for redirects bad-request (400) (400) unauthorized (401) (401) forbidden (403) (403) not-found (404) (404) internal-server-error (500) (500) And many others following HTTP status code conventions 3. Halting the routing Schematra provides helper functions for common response patterns: ; ; Redirect to another URL (get " /old-page " ( lambda ( req params ) (redirect " /new-page " ))) ; 302 redirect ; ; Halt with specific status and message (get " /admin " ( lambda ( req params ) ( if ( not (authenticated? req)) (halt 'unauthorized " Access denied " ) " Welcome to admin panel " ))) Redirect and halt both generate a specific signal that's captured by the main router and short-circuit any other processing: no other middleware or part of the route handler will be executed. HTML Responses with Chiccup For HTML responses, use the included Chiccup template system: (import chiccup) (get " /page " ( lambda ( req params ) (ccup/html `[html [head [title " My Page " ]] [body [h1 " Welcome " ] [p " This is generated HTML " ]]]))) Working with Request Bodies For POST requests, you can easily access the request body using request-body-string : (post " /submit " ( lambda ( req params ) ( let ((body (request-body-string req))) ( if ( string=? body " " ) ' (bad-request " Empty request body " ) (format " Received: ~A " body))))) Since you have access to the intarweb request object, you can also access the object and its port directly if you want. Middleware Schematra supports middleware functions that can process requests before they reach your route handlers. Middleware is useful for cross-cutting concerns like authentication, logging, request parsing, and session management. Using Middleware Install middleware using the use-middleware! function: (use-middleware! my-middleware-function) Middleware functions have the following signature: ( define ( my-middleware request params next ) ; ; Process request/params before handler ( let ((response (next))) ; Call next middleware or handler ; ; Process response after handler response)) Middleware Parameters request : The HTTP request object : The HTTP request object params : The route and query parameters alist : The route and query parameters alist next : A thunk (zero-argument function) that calls the next middleware in the chain or the final route handler Middleware Examples Simple Logging Middleware ( define ( logging-middleware request params next ) ( let* ((method (request-method request)) (uri (request-uri request)) (path (uri-path uri))) (log-dbg " ~A ~A " method (uri->string uri)) (next))) (use-middleware! logging-middleware) Authentication Middleware ( define ( valid-token? header ) ( and ( list? header) ( = 1 ( length header)) ( vector? ( car header)) ( string=? ( symbol->string (get-value ( car header))) " bearer " ) ( string=? ( symbol->string ( caar (get-params ( car header)))) " secret " ))) ; ; detail of the headers content: https://wiki.call-cc.org/eggref/5/intarweb#headers ( define ( auth-middleware request params next ) ( let ((auth-header (header-contents 'authorization (request-headers request)))) ( if ( and auth-header (valid-token? auth-header)) ; ; Continue to next middleware or route (next) ; ; Return error response ' (unauthorized " You don't belong here " )))) (use-middleware! auth-middleware) Middleware Execution Order Middleware is executed in the order it's installed with use-middleware! . The first middleware installed runs first on the request, and last on the response: (use-middleware! middleware-a) ; Runs first (use-middleware! middleware-b) ; Runs second (use-middleware! middleware-c) ; Runs third ; ; Execution flow: ; ; Request: middleware-a -> middleware-b -> middleware-c -> route-handler ; ; Response: route-handler -> middleware-c -> middleware-b -> middleware-a Built-in Middleware Schematra includes session middleware for cookie-based session management. See the "Session Management" section for details on using the session middleware. Working with Modern Web Tools Schematra plays nicely with modern web development tools: Tailwind CSS Include Tailwind via CDN in your HTML responses: (get " /tw-demo " ( lambda ( req params ) (ccup/html `[html [head [script (( " src " . " https://cdn.tailwindcss.com " ))]] [body.bg-gray-100.p-8 [h1.text-3xl.font-bold.text-blue-600 " Hello, Tailwind! " ]]]))) htmx Add htmx for dynamic interactions: (get " /htmx-demo " ( lambda ( req params ) (ccup/html `[html [head [script (( " src " . " https://cdn.jsdelivr.net/npm/[email protected]/dist/htmx.min.js " ))]] [body [button (( " hx-get " . " /clicked " ) ( " hx-target " . " #result " )) " Click me! " ] [\#result]]]))) (get " /clicked " ( lambda ( req params ) (ccup/html `[p " Button was clicked! " ]))) Session Management Schematra includes a simple session middleware that stores session data in HTTP cookies. Sessions are automatically serialized and deserialized on each request. Basic Usage First, install the session middleware with a secret key: (import sessions) ; ; Install session middleware (use-middleware! (session-middleware " your-secret-key-here " )) Then use session functions in your route handlers: (get " /login " ( lambda ( req params ) (session-set! " user-id " " 12345 " ) (session-set! " username " " alice " ) "Logged in successfully")) (get " /profile " ( lambda ( req params ) ( let ((user-id (session-get " user-id " ))) ( if user-id (format " Welcome user ~A " user-id) " Please log in " )))) (get " /logout " ( lambda ( req params ) (session-delete! " user-id " ) (session-delete! " username " ) "Logged out")) Session Functions (session-get key [default]) - Retrieve a value from the session - Retrieve a value from the session (session-set! key value) - Store a value in the session - Store a value in the session (session-delete! key) - Remove a key from the session Configuration You can customize session behavior using parameters: ; ; Set session cookie name (default: "schematra.session_id") (session-key " myapp_session " ) ; ; Set session expiration time in seconds (default: 24 hours) (session-max-age ( * 7 24 60 60 )) ; 1 week Security Notes Sessions are stored as serialized data in cookies (client-side storage) The secret key is used for session identification but not encryption Avoid storing sensitive data in sessions Consider implementing proper encryption/signing for production use Session cookies are HTTP-only by default to prevent JavaScript access Current Status This is still an exploration project! Schematra is still in early development and should not be used for production applications. Currently we have: Limited error handling Simple HTML rendering library chiccup, a hiccup-inspired rendering engine. Middleware system, with a couple of core modules to provide some basic services (sessions, csrf, oauth2) SSE support (will add WebSockets soon) No database integration, but you can use any database egg and create a middleware to provide a persistance layer. No background job system (working on one though, using the hiredis wrapper) While not ready for production, it's perfect for: Learning Scheme Prototyping simple web applications Experimenting with htmx and Tailwind CSS Understanding how web frameworks work under the hood Contributing If you find Schematra interesting and want to help it grow beyond a toy project, contributions are welcome! Feel free to: Report bugs or suggest features via GitHub issues Submit pull requests with improvements Share your experience using Schematra Help improve the documentation License This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. See the source code for the complete license text.