# 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
Welcome, {{ data.user_name }}!
{{ 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