Web Modules
The native web module provides a small router. Source modules split higher
level web application support by responsibility:
web.http: request/response/context helpers, cookies, JSON, HTML, redirects.web.router: route groups, middleware registration, decorator mounting, and dispatch.web.session: signed cookie sessions, Redis/file/database session stores, and flash messages.web.cache: Redis/file/database cache stores.web.auth: login/logout/current user helpers, role/permission helpers, and CSRF.web.validation: request JSON/form validation and validation error responses.web.forms: SSR form binding, field errors, CSRF, and flash redirects.web.middleware: CORS, security headers, request IDs, and access logging.web.sse: server-sent event formatting and typed event-stream responses.web.websocket: WebSocket upgrades, clients, and typed connection helpers.web.testing: a small dispatch client and response predicates for route tests.
Native web
new()route(app, method, path, handler),get,postbefore(app, middleware),use(app, middleware),after(app, middleware)handle(app, request)withHeader(response, name, value)
Routing And HTTP Helpers
import http;
import web.http as wh;
import web.router as router;
let app = router.newRouter();
router.get(app, "/hello/:name", func(dict<string, any> request): dict<string, any> {
let ctx = wh.context(request);
return wh.jsonStatus({"hello": ctx.param("name")}, 200);
});
http.serve("127.0.0.1:8080", func(dict<string, any> request): dict<string, any> {
return router.handle(app, request);
});
Use web.http for request builders and response helpers such as request,
requestWithBody, context, jsonResponse, jsonStatus, jsonCreated,
jsonError, html, render, redirect, normalize, statusCode,
body, header, withHeader, withCookieOptions, and deleteCookie.
Request, Response, and Context are object wrappers over the same
dictionaries used by the native router. They are conveniences, not a separate
HTTP representation:
router.get(app, "/users/:id", func(dict<string, any> request): dict<string, any> {
let req = wh.requestObject(request);
return wh.responseObject(200, {"id": req.param("id")})
.header("X-Route", "users.show")
.toDict();
});
Response inspection helpers keep middleware readable:
router.use(app, func(dict<string, any> request, dict<string, any> response): dict<string, any> {
if (wh.statusCode(response) >= 500) {
return wh.withHeader(response, "X-Error", "server");
}
return response;
});
Middleware Contracts
The router supports three middleware shapes:
router.before(app, middleware): middleware receives(request)and returnsnullto continue or a response-compatible value to stop dispatch.router.use(app, middleware): middleware receives(request, response)and returns the transformed response.router.after(app, middleware): alias for response middleware, useful when the application wants the intent to read as an after hook.
Route handlers receive (request) and may return a response dictionary, a
string, or null. web.http.normalize(value) applies the same normalization
rules used by the router: dictionaries pass through, strings become 200
responses, and null becomes 204.
Validation
web.validation builds on schema.validate and keeps handler code focused on
request input rather than parsing details:
import web.validation as validation;
let userRules = validation.object({
"name": validation.stringField(),
"age": validation.numberField()
}, ["name"]);
router.post(app, "/users", func(dict<string, any> request): dict<string, any> {
let result = validation.json(request, userRules);
if (!validation.isValid(result)) {
return validation.errorResponse(result);
}
let data = validation.data(result);
return wh.jsonCreated({"name": data["name"]});
});
Use validation.json(request, rules) for JSON APIs,
validation.form(request, rules) for form posts, and
validation.validate(data, rules) for already parsed dictionaries. The helper
rule builders cover common cases: object, stringField, intField,
numberField, boolField, arrayOf, and enumOf.
Forms
web.forms is for server-rendered forms. It binds URL-encoded form data,
validates with web.validation, exposes field-level errors, and provides
flash-friendly redirects:
import web.forms as forms;
import web.validation as validation;
let rules = validation.object({
"name": validation.stringField()
}, ["name"]);
router.post(app, "/settings", func(dict<string, any> request): dict<string, any> {
let result = forms.validate(request, rules);
if (!forms.isValid(result)) {
return wh.htmlStatus(forms.firstFieldError(result, "name"), 422);
}
return forms.redirectSuccess(sessionStore, request, "/settings", "Saved", {});
});
Use forms.bind(request) to read form data without validation,
forms.fieldErrors(result, field) for all field errors, and
forms.withCsrf/forms.verifyCsrf/forms.csrfField for CSRF workflows.
Middleware
web.middleware contains reusable response middleware:
import log;
import web.middleware as middleware;
router.use(app, middleware.securityHeaders());
router.use(app, middleware.requestId());
router.use(app, middleware.cors("https://example.com", "GET, POST", "Content-Type, Authorization"));
router.use(app, middleware.accessLog(log.stdout()));
securityHeaders()addsX-Content-Type-Options,X-Frame-Options, andReferrer-Policy.headers(values)adds custom static headers.requestId()propagates or createsX-Request-ID;requestIdHeader(name)uses a custom header name.cors(origin, methods, headers)andcorsCredentials(...)add CORS response headers.accessLog(logger)logs method, path, and status through thelogmodule.
Server-Sent Events
web.sse formats SSE frames and response dictionaries:
import web.sse as sse;
router.get(app, "/events", func(dict<string, any> request): dict<string, any> {
return sse.response([
sse.comment("ready"),
sse.named("ping", "ok"),
sse.event("user.created", "{\"id\":42}", {"id": "42"}),
sse.retry(5000)
]);
});
data(body)formats a data-only frame.named(name, body)formats an event with anevent:name.event(name, body, options)supportsidandretryfields.comment(text)andretry(milliseconds)format utility frames.response(frames)andresponseText(body)create finitetext/event-streamresponses with no-cache headers.streaming(handler)creates a long-lived streaming response. The handler receives anEventStream.write(stream, frame),flush(stream), andclose(stream)control a live stream.EventStreamalso haswrite,flush, andclosemethods.
Example:
router.get(app, "/live", func(dict<string, any> request): dict<string, any> {
return sse.streaming(func(sse.EventStream stream): void {
stream.write(sse.comment("connected"));
stream.flush();
});
});
WebSockets
web.websocket wraps the native websocket module for applications built on
web.router. Use upgrade(handler) in a route to accept a WebSocket
connection. The handler receives a Connection and can exchange text,
bytes, or JSON messages:
import web.websocket as ws;
router.get(app, "/ws", func(dict<string, any> request): dict<string, any> {
return ws.upgrade(func(ws.Connection conn): void {
let message = conn.readJson();
conn.sendJson({"echo": message["text"]});
conn.close();
});
});
Client helpers use the same module:
let conn = ws.connect("ws://127.0.0.1:8080/ws");
conn.sendText("hello");
let reply = conn.readText();
conn.close();
Available helpers are upgrade, upgradeWithHeaders, connect,
connectWithHeaders, sendText, readText, sendJson, readJson,
sendBytes, readBytes, close, and echoText. The free functions accept a
Connection; the same operations are also available as Connection methods.
Sessions, Auth, Cache
import web.auth as auth;
import web.cache as cache;
import web.http as wh;
import web.session as session;
let sessions = session.fileSessionStore("/tmp/app-sessions", 3600);
let response = auth.login(sessions, wh.text("ok"), {"name": "Ada"}, {"httpOnly": true});
let cacheStore = cache.fileCacheStore("/tmp/app-cache", 3600);
Session stores are available for Redis, files, and SQL databases.
Cache: web.cache
Import web.cache to cache expensive lookups between requests. Three store
implementations share the same interface:
| Constructor | Description |
|---|---|
cache.fileCacheStore(directory, ttl) |
File-backed store; ttl is in seconds |
cache.redisCacheStore(client, prefix, ttl) |
Redis-backed; client from the redis module |
cache.databaseCacheStore(conn, table, ttl) |
SQL-backed; call .install() once to create the table |
All stores expose get(name), set(name, value), delete(name), and
has(name).
import web.cache as cache;
import io;
let store = cache.fileCacheStore("/tmp/app-cache", 300);
func expensiveLookup(string key): string {
if (store.has(key)) {
return store.get(key) as string;
}
let result = "computed:" + key;
store.set(key, result);
return result;
}
io.println(expensiveLookup("user:1")); # computed:user:1 (first call)
io.println(expensiveLookup("user:1")); # computed:user:1 (from cache)
For Redis or database stores, pass a client handle from the appropriate module:
import redis;
import web.cache as cache;
let client = redis.connect("redis://127.0.0.1:6379");
let store = cache.redisCacheStore(client, "myapp:", 3600);
web.auth guard helpers return callable middleware:
router.before(app, auth.requireAuth(sessions));
router.before(admin, auth.requireRole(sessions, "admin"));
router.before(editor, auth.requirePermission(sessions, "posts.edit"));
router.before(account, auth.requireLogin(sessions, "/login"));
requireAuth returns a 401 JSON error for anonymous requests, requireRole
and requirePermission return 403 when the current user lacks the required
claim, and requireLogin redirects anonymous requests to the supplied path.
Use currentUser, isAuthenticated, userHasRole, and userHasPermission
when an application needs custom policy logic.
Decorator Mounting
import web.http as wh;
import web.router as router;
import web.session as session;
let sessions = session.fileSessionStore("/tmp/app-sessions", 3600);
let app = router.newRouter();
let api = router.group(app, "/api");
class AdminController {
@loginRequired
@isGranted("admin")
@route("POST", "/users")
func create(dict<string, any> request): dict<string, any> {
return wh.jsonCreated({"ok": true});
}
}
router.mountWithOptions(api, AdminController(), {"sessionStore": sessions});
Route Testing
import web.testing as webtest;
let client = webtest.client(app);
let response = client.get("/api/users");
io.println(webtest.hasStatus(response, 200));