Ten sequential spec files plus index covering runtime, observability, security, data, routing, realtime, supervisor, clients, CLI ops, and workflow, each with best practices, structure, rules, guidelines, specs, and cross-references.
21 KiB
05 - Request/Response, Routing, Views, Templates
Scope: plan.md sections 2, 6 partial, 10 step 5 partial. Topics: app, routers, views, validation/forms, negotiation, templates, middleware, DI, errors.
1. Purpose and philosophy
This file owns everything between "bytes parsed by the runtime" and "value returned by user code": the
Application composition root, FastAPI-style routers, aiohttp-style class-based views, the single
validation framework (Pydantic-style models plus HTML/JSON forms), the response hierarchy with content
negotiation, the Jinja-like template engine, the middleware onion, dependency injection without globals,
and the typed error hierarchy.
Philosophy:
- One request object, one response hierarchy. Handlers never touch sockets; they take a
Requestand return aResponse. Streaming, SSE and realtime build on the same pair (see 06-realtime.md). - Declarative over imperative. Routes, validation models, negotiation and DI bindings are declared; the framework binds, validates, negotiates and injects before user code runs.
- A single validation framework feeds path, query, header, cookie and body binding, forms, and OpenAPI schema generation (see 08-clients-openapi.md). No second ad-hoc parser anywhere.
- Templates are logic-light text renderers with secure defaults: auto-escape on for HTML/XML, explicit
safeopt-out only. - Errors are typed values, not strings. Every framework exception maps to exactly one HTTP status and renders as JSON for API clients or HTML for browsers, chosen by negotiation.
2. Best practices (from research)
- Routers group related endpoints and mount them on the app with a shared prefix, tags and dependencies.
FastAPI documents
APIRouterplusapp.include_router(router)as the scaling pattern for bigger applications, where the include call applies prefix, tags, dependencies and responses to every route in the router (Source: https://fastapi.tiangolo.com/tutorial/bigger-applications/, retrieved 2026-10-23). - Class-based views derive from a base
Viewholding the request and implement one coroutine per HTTP method (get,post, ...). aiohttp documentsclass MyView(web.View)withasync def get(self)returning a response, registered on the router like a function handler (Source: https://docs.aiohttp.org/en/stable/web_quickstart.html, retrieved 2026-10-23). - Validation guarantees the output, not the input: untrusted data is parsed and coerced into a model
instance whose fields conform to declared types and constraints, else a single error listing all
failures is raised. Pydantic v2 states models this way: fields are annotated attributes on a
BaseModelsubclass, and aValidationErroris raised when data cannot be parsed into an instance (Source: https://pydantic.dev/docs/validation/latest/concepts/models/, retrieved 2026-10-23). - Templates center on one reusable
Environmentholding loaders, filters, tests and globals; syntax is{{ ... }}for expressions,{% ... %}for statements,{# ... #}for comments, with inheritance (extends,blocks), includes,for/if, and autoescaping selectable per template name/extension (Source: https://jinja.palletsprojects.com/en/stable/templates/, retrieved 2026-10-23). - Content negotiation is server-driven: the client sends
Acceptwith media ranges and optionalqweights, the server picks one proposal and answers withContent-Type, or406 Not Acceptablewhen nothing matches (Source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Content_negotiation, retrieved 2026-10-23; triangulated by RFC 9110, Section 12, which obsoletes RFC 7231, Source: https://www.rfc-editor.org/rfc/rfc9110.html, retrieved 2026-10-23). - Middleware forms an onion: each layer sees the request before the endpoint and the response after it, and may short-circuit. Ordering rule derived from plan.md layering: exception mapping outermost, then observability, security, session/auth, routing-adjacent concerns innermost.
3. Structure
Directories (one class or concern per file, deep over flat):
src/molo/app/ Application, sub-app mounting, lifespan, plugin registry
src/molo/routing/ Router, Route, path compiler, matchers, url_for
src/molo/http_msg/ Request, Response hierarchy, Headers, Query, Cookies view
src/molo/views/ View base, method dispatch, handler adapters
src/molo/validate/ Model, Field, validators, ValidationError, coercions
src/molo/forms/ Form, JSON binding, urlencoded/multipart binding
src/molo/negotiate/ Accept parser, q-sorting, format selection
src/molo/templates/ Environment, Loader, Lexer, Parser, Renderer, Filters, Tests
src/molo/middleware/ Middleware base, Chain, built-in middlewares owned here
src/molo/di/ Container, Scope, Provider, LuckilyNoGlobals
src/molo/errors/ HttpError hierarchy, handler, renderers (JSON, HTML)
Namespaces and key classes:
molo::app::Application: owns routers, middleware chain, DI container, template env, error handler.include_router(prefix, router),mount(path, sub_app),on_startup/on_shutdownhooks.molo::routing::Router:get/post/put/patch/delete/head/options(path, handler); nestedinclude_router;Routerecord (methods, compiled path, handler, name, validation models, tags).molo::http_msg::Request: method, path, query, headers, cookies,get<T>(key)for query/form,json<T>(),form<T>(), body access with caps from 01-runtime.md/03-security.md;Responsebase plusHtmlResponse,JsonResponse,XmlResponse,TextResponse,RedirectResponse,FileResponse,StreamingResponse(streaming mechanics live in 06-realtime.md, the class shell lives here).molo::views::View: constructed withRequest; virtualget/post/...default to 405;dispatch()selects by method. Function handlers adapt to the same interface.molo::validate::Model: field descriptors with type, constraints, default, required;validate()returns model or throwsValidationErrorlisting every field failure. Used for path/query/body.molo::forms::Form: bindsapplication/json,application/x-www-form-urlencoded,multipart/form-data(file parts delegate to upload handling in 03-security.md) into aModel.molo::negotiate::Negotiator: parsesAccept, sorts by q, matches handler-declared formats.molo::templates::Environment: loaders, filters, tests, globals, autoescape predicate, cache.molo::middleware::Middleware:handle(Request, Next) -> Response;Chaincomposes in order.molo::di::Container: request-scoped and app-scoped providers, constructor injection for views.molo::errors::HttpError(status, code, message, details);ValidationHttpError(422);ErrorHandlermaps exception to status plus negotiated JSON/HTML body.
4. Rules (hard, numbered)
- R1: Every route handler is a coroutine taking
Request(or aViewholding one) and returning aResponse. No handler touches sockets, the event loop, or globals. - R2: All input binding (path, query, header, cookie, JSON body, form body) goes through
molo::validate. A second parser for the same input is a defect. - R3:
ValidationErrorlists ALL field failures in one response, never fail-fast on the first field. - R4: Responses are chosen from the
Responsehierarchy only. Raw string returns from handlers are rejected at compile time (no implicit conversion to 200 text). - R5: Template autoescaping is ON for
.html,.htm,.xmland for all streamed template output.safebypass requires naming the value safe at the call site; no global disable flag. - R6: Content negotiation runs for every response with more than one declared format. Explicit
?format=or path suffix overridesAcceptonly when the route declaresallow_format_override. No match yields406with a JSON or HTML body listing available types. - R7: Middleware order is fixed by category (errors outer, then observability, security, session/auth, user middleware, router innermost). Reordering across categories is a compile-time or startup error.
- R8: DI provides DB, cache, auth, templates and settings. No service singletons, no file-static mutable state reachable from handlers. Request scope ends at response completion.
- R9: Every framework exception maps to exactly one HTTP status via the error table in section 6. Unmapped exceptions become 500 and are logged with correlation ID; stack details never leak to clients.
- R10: Route names are unique per application (including mounted sub-apps after path merge). Duplicate names fail at startup with the colliding paths quoted.
- R11: All caps in section 6 are static constants with documented defaults. No autotuning, no silent growth.
- R12: No feature in this file duplicates scope owned by a sibling: no TLS/auth/session logic (03-security.md), no ORM/KV/cache (04-data.md), no WebSocket/SSE/Socket.IO mechanics (06-realtime.md), no OpenAPI emission (08-clients-openapi.md), no CLI (09-cli-ops.md). This file exposes the hooks those siblings consume.
5. Development guidelines
- Build in the order of section 7; each step ends green with integration tests through real HTTP.
- Prefer small composable classes: one concern per file, behavior in exactly one base class, features composed through declared interfaces (plan.md section 9).
- Headers follow the runtime: case-insensitive lookup, size and count caps enforced in 01-runtime.md before this layer sees them.
- Keep the happy path allocation-light: route match without heap allocation per request where feasible; template rendering reuses the compiled template cache.
- Every public API gets a usage example in the living example project (see 10-workflow-example.md) in the same change that introduces it.
- Docs state current behavior with gaps listed next to the code that closes them; no aspirational APIs in headers.
6. Specifications (concrete behaviors, caps, formats)
Route matching:
- Path syntax:
/users/{id:int}with convertersint,float,str(single segment),path(remainder, must be last). Unknown converter is a startup error naming the route. - Matching order: static segments first, then converters in registration order, then
pathcatch-alls. Method mismatch on an existing path yields 405 with anAllowheader, not 404. - Trailing slash: strict.
/usersand/users/are different routes unless the route setsslash_insensitive=true, which registers both and canonical-redirects (308) to the declared form. - Sub-app mount merges paths: mounting app B at
/app1inside A prefixes all B routes with/app1; triple nesting yields/app1/app1/app1automatically. Mounted apps keep their own DI scope but share the parent middleware chain plus their own appended layers.
Validation and forms:
- Field constraints:
min/max(numbers),min_len/max_len(strings/collections),pattern(regex),choices,gt/ge/lt/le,required,default,nullable. Coercion: strict for path params (no"1abc"to int), lenient-but-documented for query/form strings. ValidationErrorJSON shape:{"detail":[{"loc":["query","age"],"msg":"...","type":"..."}]}, status 422. HTML rendering shows the same list in a 422 page.- Form caps: max fields 1000, max field name 256 chars, max non-file value 1 MiB, max JSON depth 32, max JSON document 10 MiB default (overridable per route down to 0 = reject bodies). File parts use the upload quotas from 03-security.md.
Negotiation:
- Parse
Acceptinto (range, q, params); sort by q desc, then specificity (exact beat type star beat star star), then handler declaration order. MissingAcceptmeans*/*. - Handler declares
produces = {HTML, JSON}etc. First negotiated match wins. No match: 406 with{"detail":{"available":["text/html","application/json"]}}or the HTML equivalent. Content-Typeof requests must match a route-declaredconsumesset when declared, else 415.
Templates (Jinja-compatible subset, in-house engine):
- Delimiters:
{{ }},{% %},{# #}. Constructs:extends,block/endblock,include,if/elif/else/endif,for/else/endforwithloopvariable,set,macro, filters with|, tests withis,autoescapeblocks. Undefined variables render per policy:chaindefault renders empty string and logs at debug;strictmode (dev default) throwsUndefinedError(500 in prod rendering, startup warning listing the template). - Filters shipped:
escape,safe,upper,lower,trim,striptags,urlencode,length,default,join,date,tojson. Tests shipped:defined,none,string,number,sequence,mapping,even,odd. - Caps: max template size 1 MiB, max include depth 16, max extends chain 8, max loop iterations 100000, max render output 32 MiB, compiled-template cache max 512 entries with LRU eviction. Recursion beyond depth aborts with 500 and a metric increment, never a stack overflow.
- Loaders: filesystem directory, embedded assets (from 09-cli-ops.md embedder), in-memory dict. Dev reloads from
disk per render when
dev_reload=true; prod serves compiled cache only.
Middleware and DI:
- Signature:
Task<Response> handle(Request req, Next next).Nextinvokes the remainder exactly once; calling twice is a runtime error. Middleware may replace request/response but must preserve the correlation ID (owned by 02-observability.md). - Built-ins owned here:
ExceptionMappingMiddleware(outermost),NegotiationMiddleware,CORSPassthroughis NOT here (03-security.md owns CORS policy; this file only preserves its headers),RequestIdEchohelpers defer to 02-observability.md. User middleware registers with an explicit category tag. - DI scopes:
app(one per Application),request(one per request, destroyed after response). Providers are factories or values; circular dependencies fail at startup with the cycle quoted.
Errors:
- Mapping table: 400
BadRequest, 401Unauthorized, 403Forbidden, 404NotFound(includesurl_formiss at render time), 405MethodNotAllowed(setsAllow), 406NotAcceptable, 408/413/414/431 from runtime caps pass through, 415UnsupportedMediaType, 422ValidationFailed, 429 from 03-security.md passes through, 500InternalError, 503Unavailable. - JSON error shape:
{"error":{"code":"not_found","message":"...","request_id":"..."}}. HTML error shape: responsive error page with the same fields, no internals. - Redirects:
RedirectResponse(url, permanent)emits 308/307 (method-preserving). Flash messages ride the session (03-security.md) viaflash(request, level, message)and render once.
Global caps (static, overridable by config, never autotuned):
- max routes per app 100000; max path length 2048 chars; max path params per route 32; max middleware layers 64; max sub-app nesting depth 8; max DI providers 4096.
7. Execution steps (in build order)
- E1:
Request,Headers,Query, cookie view,Responsebase plus Text/JSON/HTML/XML/Redirect. No routing yet; handlers invoked directly in tests. - E2: Path compiler plus
Router(static, converters, 404/405, Allow header),url_forby name. - E3:
Application,include_routerwith prefix/tags/dependencies, sub-appmountwith path merge, startup validation (duplicate names, unknown converters, cycles). - E4:
molo::validate(Model, Field, coercions, full-errorValidationError) bound to path, query, headers, cookies. - E5:
molo::errors(hierarchy, mapping table, JSON plus HTML renderers, global handler). - E6:
molo::negotiateplusNegotiationMiddleware,consumes/producesdeclarations, 406/415 paths. - E7:
molo::views::Viewwith method dispatch, function-handler adapter, redirects, flash helpers. - E8: Middleware
Chainwith category ordering enforcement andNext-once guard. - E9:
molo::di(app and request scopes, cycle detection) wired into views and handlers. - E10:
molo::forms(JSON, urlencoded, multipart field parts) on top of validation. - E11: Template engine: lexer, parser, renderer, Environment, loaders, filters/tests, autoescape,
caps and compiled cache.
TemplateResponse(template, context)as aResponsesubclass. - E12: Plugin/extension registry on
Application(named hooks: route_added, app_started, error_render) with capability declarations so plugins cannot reach undeclared internals.
8. Validation (how to verify each step)
Integration tests only, through the real stack (real HTTP, routing, middleware), per plan.md section 8.
Test files mirror routes: tests/routes/<router>/<route>_test.cpp.
- V1: GET/POST round-trips return correct status, headers and bodies for each response subclass.
- V2: static, converter and catch-all matches; 404 body shape; 405 with exact
Allowvalue;url_forreverses every named route including converted params. - V3: prefixed includes; triple-nested mount produces
/app1/app1/app1; duplicate names and unknown converters fail startup with quoted diagnostics. - V4: valid input binds; invalid input yields one 422 listing every bad field (path, query, header, cookie cases each covered).
- V5: each mapped exception renders the exact JSON shape and the HTML page; unknown exception yields
500 JSON/HTML without internals;
request_idpresent in both. - V6: q-weighted
Acceptfixtures pick the expected format; no-match yields 406 listing available types; wrongContent-Typeyields 415. - V7: View dispatches each method; unimplemented method yields 405; redirects preserve method (307/308); flash message appears exactly once across two requests.
- V8: middleware order test asserts onion sequence; double-
Nextcall fails loudly; short-circuit returns without reaching the handler. - V9: request scope isolation across two concurrent requests; app scope shared; cycle fixture fails startup quoting the cycle.
- V10: JSON, urlencoded and multipart forms bind to the same model; cap fixtures (1001 fields, depth 33, oversized value) are rejected with the documented status.
- V11: template fixtures cover extends/blocks, include, if/for/else, filters, tests, macros; XSS
fixture asserts escaping;
safefixture asserts opt-out; cap fixtures (depth 17, 100001 iterations) abort safely; dev reload picks up an edited file, prod serves cache. - V12: sample plugin receives hooks in order and is denied undeclared access.
9. References (sibling files this depends on or feeds)
- Depends on 01-runtime.md: timeouts, body/header caps, cookies, compression, static serving. This file never re-enforces byte-level caps.
- Depends on 02-observability.md: correlation IDs, log format, per-route metrics, tracing spans.
- Depends on 03-security.md: CSRF, CORS policy, rate limits, session/flash storage, upload quotas, auth guards consumed by views and DI.
- Depends on 04-data.md: ORM/KV/cache handles provided through DI; explicit invalidation hooks.
- Feeds 06-realtime.md:
Request/Responseshells, router mounting and middleware/DI participation for WebSocket/SSE/Socket.IO views and streaming responses. - Feeds 07-supervisor-replication.md: request scope teardown points safe for background handoff.
- Feeds 08-clients-openapi.md: routers plus validation models as the OpenAPI source; response formats
as client expectations; test client drives
Applicationwithout network. - Feeds 09-cli-ops.md: dev server hosts
Application; embedder output plugs into template loaders; health endpoints are plain routes defined here. - Follows 10-workflow-example.md: specs, test layout, living example coverage for every API above.
10. Open questions
- Q1: Exact lenient coercion table for query/form strings (e.g.
"yes"to bool, empty string to null). Proposal: strict booleans (true/false/1/0only), empty string never coerces to null. - Q2: Whether
?format=override defaults on or off. Proposal: off; routes opt in. - Q3: Template
strictundefined policy in production: render empty plus warn, or 500. Proposal: empty plus warn and metric, 500 only in dev/test. - Q4: Plugin capability model granularity: coarse hook names now, fine-grained review before v1.
11. Sources (exact URLs with retrieval dates)
- FastAPI tutorial, Bigger Applications (APIRouter, include_router): https://fastapi.tiangolo.com/tutorial/bigger-applications/ (retrieved 2026-10-23)
- aiohttp Web Server Quickstart (Application, handlers, class-based views): https://docs.aiohttp.org/en/stable/web_quickstart.html (retrieved 2026-10-23)
- Pydantic v2 concepts, Models (validation guarantees, ValidationError): https://pydantic.dev/docs/validation/latest/concepts/models/ (retrieved 2026-10-23)
- Jinja Template Designer Documentation (syntax, inheritance, autoescape): https://jinja.palletsprojects.com/en/stable/templates/ (retrieved 2026-10-23)
- MDN, Content negotiation (Accept, q weights, 406): https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Content_negotiation (retrieved 2026-10-23)
- RFC 9110, HTTP Semantics, Section 12 (content negotiation, obsoletes RFC 7231): https://www.rfc-editor.org/rfc/rfc9110.html (retrieved 2026-10-23)