# HTML API The **HTML API** exposes `@api(...)` events as server-rendered HTML pages using Jinja2 templates. --- ## How HTML rendering works ```mermaid flowchart TD C["Client (Browser)
GET /dashboard"] --> L[API Listener] L --> Match{URL matches an event?} Match -->|no| Static["try static file from EDRI_ASSETS_PATH"] Static --> Found{found?} Found -->|yes| Serve["serve file"] Found -->|no| E404["404"] Match -->|"yes
create Event
route to handler"| Resp[Receive Response] Resp --> Redir{RedirectResponseDirective?} Redir -->|yes| R302["HTTP 302 redirect"] Redir -->|no| Tmpl{template defined?} Tmpl -->|yes| Render["load Jinja2 template
render data
status + scope"] --> R200["HTTP 200 text/html"] Tmpl -->|no| Acc{"client accepts JSON?
Accept */*
or application/json"} Acc -->|yes| JSON["serve JSON instead
before HTML rendering"] Acc -->|"no (Accept: text/html)"| E406["406 Not Acceptable"] ``` > The JSON fallback for template-less events is decided by the Listener before the HTML renderer runs — see {doc}`/api_content_negotiation`. --- ## Defining an HTML event HTML exposure requires `template` and `method`; `url` is optional and defaults to `/{snake_case_class_name}`: ```python from http import HTTPMethod from edri.api.dataclass.api_event import api, ApiType from edri.dataclass.event import Event from edri.dataclass.response import Response, response @response class DashboardResponse(Response): user_name: str items: list[dict] @api( url="/dashboard", template="dashboard.j2", method=HTTPMethod.GET, exclude={ApiType.WS}, ) class Dashboard(Event): response: DashboardResponse ``` Notes: * `method` is set on the `@api(...)` decorator, not as an event field. * `exclude={ApiType.WS}` is typical — HTML events rarely need WebSocket exposure. * `exclude={ApiType.REST}` is also common if you don't want JSON responses for this endpoint. --- ## Template rendering context When EDRI renders a Jinja2 template, the following variables are available: | Variable | Type | Content | |-------------|-----------------|-----------------------------------------------------------------------| | `data` | dict | Response fields (wrapped: `{"field_name": value, ...}`) | | `status` | str | Response status (`OK`, `FAILED`, or `NONE`) | | `scope` | dict | ASGI scope (path, method, headers, etc.) | > **Note:** HTML templates always render response fields under `data` because `HTMLHandler` calls `Response.as_dict(..., wrapped=True)`. The `EDRI_API_RESPONSE_WRAPPED` setting affects REST/WS JSON responses only, not the HTML template context. Field names are **not** camelCase-transformed in HTML templates (unlike REST/WS). ### Example template ```html+jinja

Dashboard

Welcome, {{ data.user_name }}!

``` --- (url-generation-in-templates)= ## URL generation in templates The `{% url %}` Jinja2 extension generates URLs from event class names, keeping URL patterns centralized. ### Basic usage ```html Dashboard ``` ### With path parameters ```html User #42 ``` ### With optional trailing parameters ```python @api(url="/user/group/{user_id}/{group_id}", method=HTTPMethod.GET) class UserGroup(Event): user_id: int group_id: int | None = None # optional → trailing ``` ```html Group overview Group detail ``` ### URL generation rules 1. The first argument is the **event class name** (Python class name). 2. Keyword arguments fill URL path placeholders. 3. Required placeholders must be provided — missing ones raise an error. 4. Optional placeholders (declared as `T | None`) may be omitted — the trailing segment is dropped. 5. Optional parameters must be **trailing** (after all required parameters). --- ## Static files A request URL that no event route claims is served from `EDRI_ASSETS_PATH` (default `assets/`), mapping the URL path one-to-one onto the directory: `assets/static/css/style.css` is served at `/static/css/style.css`. Reference assets from templates with root-absolute paths: ```html+jinja ``` Static files are public (middleware does not run for them), are served without caching headers, and are not served to JSON clients. See {doc}`/static_files` for resolution rules, response headers, access control, and production setup. --- ## Jinja2 configuration ### Template directories EDRI looks for templates in `EDRI_TEMPLATE_PATH` (default: `templates/`). ``` templates/ ├── dashboard.j2 ├── auth/ │ ├── login.j2 │ └── register.j2 └── contact/ ├── form.j2 └── thanks.j2 ``` ### Autoescaping EDRI configures Jinja2 with `select_autoescape(enabled_extensions=("html", "xml", "j2"), default_for_string=True, default=True)`. Autoescaping is therefore **on by default** for `.html`, `.xml`, and `.j2` templates as well as for templates rendered from strings, so rendered values are HTML-escaped and protected against injection unless you explicitly mark them safe (`| safe`). ### Custom Jinja2 extensions EDRI provides a way to register custom Jinja2 extensions: ```python edri.start_api( middlewares=[...], jinja_extensions={MyCustomExtension}, ) ``` Built-in extensions: | Extension | Tag/Filter | Purpose | |-----------------|------------|---------------------------------------| | `URLExtension` | `{% url %}` | Generate URLs from event class names | > To put many endpoints under a shared URL/resource prefix, use `PrefixBase` instead — it is an event base class, not a Jinja extension, and applies to REST/WS/HTML alike. See {ref}`Shared URL/resource prefixes `. --- ## Post/Redirect/Get pattern A common HTML pattern: the browser sends a form (POST), the server validates it and, on success, returns a **redirect** (302) so the browser issues a GET to the success page. This prevents form re-submission on refresh. ### Manager implementation ```python from edri.abstract import ManagerBase, request from edri.dataclass.directive.html import RedirectResponseDirective from edri.dataclass.response import ResponseStatus class ContactManager(ManagerBase): @request def submit(self, event: ContactSubmit) -> None: # ... business logic ... # On success: redirect to thank-you page event.response.set_status(ResponseStatus.OK) event.response.add_directive( RedirectResponseDirective(ContactThanks) ) ``` The template is **not** rendered when a redirect directive is present — instead, the HTTP 302 response is sent directly. --- ## Post/Redirect/Get with validation failure When validation fails and you want to re-render the form with an error message: ```python @request def submit(self, event: ContactSubmit) -> None: if business_validation_fails(event): event.response.error = "Invalid input" event.response.set_status(ResponseStatus.FAILED) return # ← template is rendered, error shown to user # On success: event.response.add_directive(RedirectResponseDirective(ContactThanks)) ``` In the template: ```html+jinja {% if data.error %}

{{ data.error }}

{% endif %} ``` --- ## Gotchas & Notes * HTML exposure requires `template` and `method`; `url` defaults to `/{snake_case_class_name}` if not provided. Without `method`, both REST and HTML are excluded automatically. * Optional path parameters must be **trailing** (after all required parameters). * `RedirectResponseDirective` suppresses template rendering — the redirect is sent instead. * `{% url %}` uses the event **class name** (string), not the URL path. * Static file serving does **not** support directory indexes — you must request the exact filename. * Event routes take precedence over static files; a dynamic route such as `/{slug}` shadows every file at that depth. See {ref}`static-files-resolution`. * Custom Jinja2 extensions are registered per `EDRI` instance (not global). * The `method` parameter belongs on `@api(...)`, not as an event field. --- ## See also * {doc}`/api` — API layer overview * {doc}`/api_event` — `@api(...)` decorator reference * {doc}`/response` — response directives (redirects, cookies) * {doc}`/static_files` — serving static assets * {doc}`/examples/html/form_post_redirect` — complete example * {doc}`/env` — `EDRI_TEMPLATE_PATH`, `EDRI_ASSETS_PATH` variables