@node Guidance @chapter Guidance This chapter explains how the pieces of Safsaf fit together. Each section covers one concept with a short code example. For the full list of parameters and options, see @ref{API}. @menu * Getting Started:: A minimal runnable server. * Routing:: Route patterns, groups, and reverse routing. * Handler Wrappers:: Composing middleware via wrap-routes. * Responses:: HTML, JSON, text, redirects, and errors. * Request Parsing:: Forms, query strings, multipart, cookies. * Parameter Parsing:: Declarative param specs with validation. * Sessions:: Signed cookie sessions. * Templating:: Streaming HTML with dynamic slots. * Static Files:: Serving files from disk. @end menu @node Getting Started @section Getting Started A Safsaf application is a list of routes passed to @code{run-safsaf}. Each route binds an HTTP method and URL pattern to a handler procedure. The handler receives a Guile @code{} and a body port, and returns two values: a response and a body. @lisp (use-modules (safsaf) (safsaf router) (safsaf response-helpers)) (define routes (list (route 'GET '() index-page #:name 'index) (route 'GET '("hello" name) hello-page) (route '* '* (lambda (request body-port) (not-found-response))))) (define (index-page request body-port) (html-response '(h1 "Welcome"))) (define (hello-page request body-port) (let ((name (assoc-ref (current-route-params) 'name))) (text-response (string-append "Hello, " name "!")))) (run-safsaf routes #:port 8080) @end lisp The last route should be a catch-all (@code{'*} method, @code{'*} pattern) so that every request is handled. @code{run-safsaf} sets up a Fibers scheduler, starts the HTTP server, and blocks until Ctrl-C. @node Routing @section Routing @subheading Patterns Route patterns are lists of segments. A string matches literally, a symbol captures that segment into @code{current-route-params}, and a two-element list @code{(predicate name)} captures only when @var{predicate} returns true. A dotted tail captures the remaining path. @lisp ;; Literal: /about (route 'GET '("about") about-handler) ;; Capture: /users/:id (route 'GET '("users" id) show-user) ;; Predicate: /posts/:id where id is numeric (route 'GET '("posts" (,string->number id)) show-post) ;; Wildcard (rest): /files/* — captures remaining segments (route 'GET '("files" . path) serve-file) @end lisp @subheading Route groups @code{route-group} nests routes under a shared prefix: @lisp (route-group '("api" "v1") (route 'GET '("users") api-list-users) (route 'GET '("users" id) api-show-user)) @end lisp This matches @code{/api/v1/users} and @code{/api/v1/users/:id}. @subheading Named routes and path-for Give a route a @code{#:name} and use @code{path-for} to generate its URL, so paths are never hard-coded. The first argument is always a route group: @lisp (define my-routes (route-group '() (route 'GET '("posts" id) show-post #:name 'show-post))) (define all-routes (list my-routes (route '* '* (lambda (r b) (not-found-response))))) ;; In a handler or template: (path-for my-routes 'show-post '((id . "42"))) ;; => "/posts/42" @end lisp @code{path-for} also accepts @code{#:query} and @code{#:fragment} keyword arguments. @node Handler Wrappers @section Handler Wrappers A handler wrapper is a procedure that takes a handler and returns a new handler. It can transform the request on the way in and the response on the way out. Apply wrappers to a route tree with @code{wrap-routes}. @lisp (wrap-routes routes (make-exceptions-handler-wrapper #:dev? #t) logging-handler-wrapper) @end lisp When multiple wrappers are given, the first wraps outermost — it runs first on the request and last on the response. In the example above, exceptions catches errors from the logging wrapper and the inner handler. @subheading Per-group wrappers Apply wrappers to part of the route tree by wrapping a group separately: @lisp (define api-routes (wrap-routes (route-group '("api") (route 'GET '("items") api-list-items)) cors-handler-wrapper)) (define all-routes (wrap-routes (list api-routes (route 'GET '() index-page) (route '* '* (lambda (r b) (not-found-response)))) logging-handler-wrapper)) @end lisp Here CORS headers are added only to @code{/api/*} routes, while logging applies to everything. @subheading Security headers @code{security-headers-handler-wrapper} @emph{appends} its headers to the response rather than replacing existing ones. If a handler sets @code{X-Frame-Options} itself, both values will appear in the response. To avoid duplication, either omit the header from the wrapper (pass @code{#:frame-options #f}) or do not set it in the handler. @subheading Max body size @code{make-max-body-size-handler-wrapper} checks the @code{Content-Length} header and rejects requests that exceed the limit with a 413 response. However, it does @emph{not} limit chunked transfer-encoded requests that lack @code{Content-Length}. For untrusted networks, use a reverse proxy (e.g.@: Nginx's @code{client_max_body_size}) to enforce size limits at the transport level. @node Responses @section Responses Safsaf provides helpers that return @code{(values response body)} directly: @lisp ;; HTML — streams an SXML tree (html-response '(div (h1 "Hello") (p "world"))) ;; JSON — takes a JSON string (json-response (scm->json-string '(("ok" . #t)))) ;; Plain text (text-response "pong") ;; Redirect (default 303 See Other) (redirect-response "/login") (redirect-response "/new-item" #:code 302) ;; Error responses (not-found-response) (bad-request-response "Missing field") (forbidden-response) @end lisp @code{html-response}, @code{json-response}, @code{text-response}, and @code{redirect-response} accept @code{#:code} and @code{#:headers} for overrides. The error helpers (@code{not-found-response}, etc.)@: accept @code{#:headers} but have a fixed status code. For content negotiation, use @code{negotiate-content-type}: @lisp (define (show-item request body-port) (let ((item (fetch-item (assoc-ref (current-route-params) 'id)))) (case (negotiate-content-type request '(text/html application/json)) ((application/json) (json-response (scm->json-string (item->alist item)))) (else (html-response `(div (h1 ,(item-title item)))))))) @end lisp @node Request Parsing @section Request Parsing @subheading Form bodies @code{parse-form-body} reads a URL-encoded POST body and returns an alist of string pairs: @lisp (define (handle-login request body-port) (let* ((form (parse-form-body request body-port)) (username (assoc-ref form "username")) (password (assoc-ref form "password"))) (if (valid-credentials? username password) (redirect-response "/dashboard") (text-response "Invalid login" #:code 401)))) @end lisp @subheading Query strings @code{parse-query-string} extracts query parameters from the request URL: @lisp (let ((qs (parse-query-string request))) (assoc-ref qs "page")) ;; => "2" or #f @end lisp @subheading Multipart For file uploads, use @code{parse-multipart-body}: @lisp (let* ((parts (parse-multipart-body request body-port)) (form (multipart-text-fields parts)) (file (parts-ref parts "avatar"))) ;; form is an alist of text fields ;; file is a record — read its body with (part-body file) ...) @end lisp @subheading Cookies Read cookies with @code{request-cookie-ref} or @code{request-cookies}. Set them via response headers with @code{set-cookie-header} and @code{delete-cookie-header}: @lisp (request-cookie-ref request "theme") ;; => "dark" or #f (text-response "ok" #:headers (list (set-cookie-header "theme" "dark" #:path "/" #:http-only #t))) @end lisp @node Parameter Parsing @section Parameter Parsing @code{parse-params} validates and transforms raw form or query data according to a declarative spec. Each spec entry names a parameter, a processor (a procedure that converts a string or returns an @code{}), and options like @code{#:required} or @code{#:default}. @lisp (let ((params (parse-params `((page ,as-integer #:default 1) (per-page ,as-integer #:default 20) (q ,as-string)) (parse-query-string request)))) (assq-ref params 'page)) ;; => 1 (integer, not string) @end lisp Built-in processors: @code{as-string}, @code{as-integer}, @code{as-number}, @code{as-checkbox}, @code{as-one-of}, @code{as-matching}, @code{as-predicate}. @subheading Form params with CSRF For POST forms, use @code{parse-form-params} instead — it automatically checks the CSRF token (from @code{csrf-handler-wrapper}) before parsing: @lisp (let* ((form (parse-form-body request body-port)) (params (parse-form-params `((title ,as-string #:required) (body ,as-string #:required)) form))) (if (any-invalid-params? params) ;; Re-render the form with errors (render-form (field-errors params 'title) (field-errors params 'body)) ;; Proceed (create-item! (assq-ref params 'title) (assq-ref params 'body)))) @end lisp @code{any-invalid-params?} returns @code{#t} if any value failed validation. @code{field-errors} returns a list of error message strings for a given field, suitable for rendering next to form inputs. @node Sessions @section Sessions Sessions use HMAC-signed cookies via @code{(webutils sessions)}. Set up a session config and apply the wrapper: @lisp (define session-mgr (make-session-config "my-secret-key" #:cookie-name "my-session")) (define routes (wrap-routes my-routes (make-session-handler-wrapper session-mgr))) @end lisp Inside a handler, @code{(current-session)} returns the session data (an alist) or @code{#f} if no valid session exists. To set session data, include a @code{session-set} header in the response. To delete, use @code{session-delete}: @lisp ;; Set session (redirect-response "/" #:headers (list (session-set session-mgr '((user-id . 42))))) ;; Read session (let ((user-id (and (current-session) (assoc-ref (current-session) 'user-id)))) ...) ;; Delete session (redirect-response "/" #:headers (list (session-delete session-mgr))) @end lisp @node Templating @section Templating @code{write-shtml-as-html/streaming} works like htmlprag's @code{write-shtml-as-html}, but any procedure in the SHTML tree is called as @code{(proc port)} and can write dynamic content directly. @code{streaming-html-response} wraps this into a response: give it an SHTML tree (with optional procedure slots) and it returns @code{(values response body)} ready for a handler. @lisp (define (base-layout title content-proc) `(*TOP* (*DECL* DOCTYPE html) (html (head (title ,title)) (body (nav (a (@@ (href "/")) "Home")) (main ,content-proc) (footer (p "Footer")))))) @end lisp The layout is plain SHTML with a procedure in the @var{content-proc} position. Use @code{streaming-html-response} to send it: @lisp (define (index-page request body-port) (streaming-html-response (base-layout "Home" (lambda (port) (write-shtml-as-html `(div (h1 "Welcome") (p "Content goes here.")) port))))) @end lisp You can also call @code{write-shtml-as-html/streaming} directly when you need to write SHTML with procedure slots to an arbitrary port. @node Static Files @section Static Files @code{make-static-handler} returns a handler that serves files from a directory. Pair it with a wildcard route: @lisp (route-group '("static") (route 'GET '(. path) (make-static-handler "./public" #:cache-control '((max-age . 3600))))) @end lisp This serves @code{/static/css/style.css} from @code{./public/css/style.css}. The handler supports @code{If-Modified-Since} for 304 responses.