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, post
  • before(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 returns null to 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() adds X-Content-Type-Options, X-Frame-Options, and Referrer-Policy.
  • headers(values) adds custom static headers.
  • requestId() propagates or creates X-Request-ID; requestIdHeader(name) uses a custom header name.
  • cors(origin, methods, headers) and corsCredentials(...) add CORS response headers.
  • accessLog(logger) logs method, path, and status through the log module.

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 an event: name.
  • event(name, body, options) supports id and retry fields.
  • comment(text) and retry(milliseconds) format utility frames.
  • response(frames) and responseText(body) create finite text/event-stream responses with no-cache headers.
  • streaming(handler) creates a long-lived streaming response. The handler receives an EventStream.
  • write(stream, frame), flush(stream), and close(stream) control a live stream. EventStream also has write, flush, and close methods.

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