AtlatestRepositorysigil-web
1# Routing
2
3> Request routing, middleware, cookies, and static files.
4
5```scheme
6(import (sigil web)
7 (sigil http))
8```
9
10The `(sigil web)` module re-exports all routing, middleware, cookie, and static file functionality. Import sub-modules directly for selective access:
12| Module | Purpose |
13|--------|---------|
14| `(sigil web routes)` | Path-based routing |
15| `(sigil web middleware)` | Middleware composition |
16| `(sigil web cookies)` | Cookie parsing and setting |
17| `(sigil web static)` | Static file serving |
19## Routes
21Define routes with an HTTP method, URL pattern, and handler. Handlers receive a
22request and return a response (or `#f` for no match). `method:` takes an
23HTTP-method **symbol** and defaults to `POST` when omitted (a `"post"` string is
24accepted too).
26```scheme
27(define app
28 (router
29 (route method: GET pattern: "/" handler: home-handler)
30 (route method: GET pattern: "/about" handler: about-handler)
31 (route method: POST pattern: "/api/login" handler: login-handler)))
32```
34Available method symbols: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `OPTIONS`,
35`HEAD`, `ANY`. `ANY` matches all HTTP methods.
37## Pattern Matching
39Route patterns support three segment types:
41| Pattern | Matches | Example |
42|---------|---------|---------|
43| `/users` | Literal path | `/users` only |
44| `/users/:id` | Named parameter | `/users/42`, `/users/alice` |
45| `/static/*path` | Wildcard (rest of path) | `/static/css/main.css` |
47```scheme
48(router
49 (route method: GET pattern: "/users/:id" handler: user-handler)
50 (route method: GET pattern: "/files/*path" handler: file-handler))
51```
53### Path Parameters
55Extract parameters from matched routes using `path-param`:
57```scheme
58(define (user-handler request)
59 (let ((id (path-param request "id")))
60 (http-response/html 200
61 (string-append "<h1>User " id "</h1>"))))
63;; Get all params as an alist
64(define (handler request)
65 (let ((params (path-params request)))
66 ...))
67```
69## Route Combination
71`routes` combines multiple handlers into one. The first handler to return a
72non-`#f` response wins.
74```scheme
75(define api-routes
76 (router
77 (route method: GET pattern: "/api/users" handler: list-users)
78 (route method: POST pattern: "/api/users" handler: create-user)))
80(define page-routes
81 (router
82 (route method: GET pattern: "/" handler: home-page)
83 (route method: GET pattern: "/about" handler: about-page)))
85(define app
86 (routes api-routes page-routes))
87```
89### ui-routes
91`ui-routes` is a `routes` analog for hypermedia handlers: it combines handlers
92the same way, but a handler that returns a value which **isn't** an HTTP response
93is taken as "handled, nothing extra for the acting user" and becomes an empty
94`(ui-response)`. So an action handler can end with just its mutation, no trailing
95`(ui-response)`:
97```scheme
98(define (toggle-handler req)
99 (live-update! todos (item-id req)
100 (lambda (i) (dict-set i done?: (not (dict-ref i done?:)))))) ; no ui-response needed
102(define app
103 (-> (routes
104 (ui-routes (router …hypermedia…)) ; non-response returns -> empty ack
105 (router …json-api…)) ; plain routes never coerce
106 (with-sigil-ui)
107 (with-logging)
108 (with-not-found)))
109```
111`#f` still falls through (a missing-id mutation → 404 via `with-not-found`), and
112explicit responses pass through untouched. Keep JSON APIs and webhooks under
113plain `routes`/`router`, which never coerce. `ui-routes` is exported from
114`(sigil web ui)`.
116## Middleware
118Middleware wraps a handler to add cross-cutting concerns. Two styles are supported.
120### Chain Style
122Chain-style wrappers take a handler and return a wrapped handler, ideal for use with the `->` threading macro:
124```scheme
125(define app
126 (-> (routes api-routes page-routes)
127 (with-logging)
128 (with-cors)
129 (with-content-type)
130 (with-not-found)))
131```
133| Wrapper | Keywords | Description |
134|---------|----------|-------------|
135| `with-logging` | `output:` | Log requests with method, path, status, duration |
136| `with-cors` | `allow-origin:`, `allow-methods:`, `allow-headers:` | Add CORS headers (default: allow all origins) |
137| `with-content-type` | `default:` | Set default Content-Type (default: `"text/html"`) |
138| `with-not-found` | `body:` | Return 404 if handler returns `#f` |
139| `with-sigil-ui` | `path:` | Serve the client script at `/js/sigil-web-ui.js` (from `(sigil web ui)`) |
141```scheme
142;; Custom CORS settings
143(-> handler
144 (with-cors allow-origin: "https://example.com"
145 allow-methods: "GET, POST"))
147;; Custom 404 page
148(-> handler
149 (with-not-found body: "<h1>Page not found</h1>"))
150```
152### Factory Style
154Factory-style middleware returns a `handler -> handler` function, useful for `wrap-middleware`:
156```scheme
157(define app
158 (wrap-middleware handler
159 (logger-middleware)
160 (cors-middleware '("*"))
161 (not-found-middleware)))
162```
164## Cookies
166Parse cookies from requests and set cookies on responses.
168```scheme
169;; Get a specific cookie
170(cookie-ref request "session_id")
171; => "abc123" or #f
173;; Get all cookies as a dict
174(cookies request)
175; => #{ "session_id": "abc123" "theme": "dark" }
177;; Set a cookie on a response
178(set-cookie response "session_id" "abc123"
179 '((path . "/")
180 (http-only . #t)
181 (max-age . 3600)
182 (same-site . "Strict")))
184;; Delete a cookie (sets max-age=0)
185(delete-cookie response "session_id")
186```
188Cookie options:
190| Option | Description |
191|--------|-------------|
192| `path` | Cookie path (e.g., `"/"`) |
193| `domain` | Cookie domain |
194| `max-age` | Seconds until expiration |
195| `expires` | Expiration date string |
196| `secure` | Only send over HTTPS (`#t`/`#f`) |
197| `http-only` | Not accessible via JavaScript (`#t`/`#f`) |
198| `same-site` | `"Strict"`, `"Lax"`, or `"None"` |
200## Static Files
202Serve files from a directory with automatic MIME type detection.
204```scheme
205(define static-handler
206 (make-static-handler "public/"))
208;; Use in a router with a wildcard pattern
209(define app
210 (-> (routes
211 (router (route method: GET pattern: "/" handler: home-handler))
212 (make-static-handler "public/"
213 '((prefix . "/static"))))
214 (with-not-found)))
215```
217Options for `make-static-handler`:
219| Option | Default | Description |
220|--------|---------|-------------|
221| `index` | `"index.html"` | Index file for directory requests |
222| `prefix` | `""` | URL prefix to strip before file lookup |
224`serve-file` serves a single file by path, returning `#f` if it doesn't exist:
226```scheme
227(serve-file "public/favicon.ico")
228```
230Path traversal attacks (`..`) are blocked by `safe-path?`.
232## Common Patterns
234### Full Application Setup
236```scheme
237(import (sigil web)
238 (sigil http))
240(define (home-handler request)
241 (http-response/html 200 "<h1>Home</h1>"))
243(define (user-handler request)
244 (let ((id (path-param request "id")))
245 (http-response/json 200 #{ id: id }))) ; a dict is encoded for you
247(define app
248 (-> (routes
249 (router
250 (route method: GET pattern: "/" handler: home-handler)
251 (route method: GET pattern: "/users/:id" handler: user-handler))
252 (make-static-handler "public/"))
253 (with-logging)
254 (with-cors)
255 (with-not-found)))
257(http-serve app port: 8080)
258```