|
| 1 | +# Rendering APIs with HTMX |
| 2 | + |
| 3 | +This guide demonstrates how to enhance a basic `holm` application with a rendering API and HTMX for dynamic, interactive web applications. We'll build upon the [quick start guide](quick-start-guide.md) to add server-rendered partial updates and seamless navigation. |
| 4 | + |
| 5 | +Very basic familiarity with [HTMX](https://htmx.org/) is helpful for this guide, but it can be followed even without it. |
| 6 | + |
| 7 | +Before you continue, make sure you have the basic application from the [quick start guide](quick-start-guide.md) working, as we will be expanding that application. |
| 8 | + |
| 9 | +We will cover: |
| 10 | + |
| 11 | +- How to integrate HTMX for dynamic content updates. |
| 12 | +- How to enhance navigation with the `hx-boost` HTMX attribute. |
| 13 | +- How to create API endpoints that return rendered HTML components, often called fragments or partials. |
| 14 | +- How to use [fasthx](https://volfpeter.github.io/fasthx/examples/htmy/) for seamless server-side rendering of [htmy](https://volfpeter.github.io/htmy/) components. |
| 15 | + |
| 16 | +The entire source code of this application can be found in the [examples/rendering-apis-with-htmx](https://github.com/volfpeter/holm/tree/main/examples/rendering-apis-with-htmx) directory of the repository. |
| 17 | + |
| 18 | +## Add HTMX to the application |
| 19 | + |
| 20 | +Modify `layout.py` to include the HTMX script and enable `hx-boost` on the `nav` tag for enhanced navigation: |
| 21 | + |
| 22 | +```python |
| 23 | +from htmy import Component, ComponentType, Context, component, html |
| 24 | + |
| 25 | +from holm import Metadata |
| 26 | + |
| 27 | + |
| 28 | +@component |
| 29 | +def layout(children: ComponentType, context: Context) -> Component: |
| 30 | + """Root layout wrapping all pages.""" |
| 31 | + metadata = Metadata.from_context(context) |
| 32 | + |
| 33 | + return ( |
| 34 | + html.DOCTYPE.html, |
| 35 | + html.html( |
| 36 | + html.head( |
| 37 | + html.title(metadata.get("title", "My App")), |
| 38 | + html.meta(charset="utf-8"), |
| 39 | + html.meta(name="viewport", content="width=device-width, initial-scale=1"), |
| 40 | + html.link( # Use PicoCSS to add some default styling. |
| 41 | + rel="stylesheet", href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css" |
| 42 | + ), |
| 43 | + html.script(src="https://unpkg.com/htmx.org@2.0.7"), |
| 44 | + ), |
| 45 | + html.body( |
| 46 | + html.header( |
| 47 | + html.nav( |
| 48 | + html.ul( |
| 49 | + html.li(html.a("Home", href="/")), |
| 50 | + html.li(html.a("About", href="/about")), |
| 51 | + ), |
| 52 | + hx_boost="true", |
| 53 | + ), |
| 54 | + class_="container", |
| 55 | + ), |
| 56 | + html.main(children, class_="container"), |
| 57 | + html.footer(html.p("© 2025 My App"), class_="container"), |
| 58 | + class_="container-fluid", |
| 59 | + ), |
| 60 | + ), |
| 61 | + ) |
| 62 | +``` |
| 63 | + |
| 64 | +The changes in the layout are trivial, we simply: |
| 65 | + |
| 66 | +- added the HTMX script tag to the `head` element of the webpage with `html.script(src="https://unpkg.com/htmx.org@2.0.7")`; |
| 67 | +- and set the `hx_boost` attribute on the `nav` element in the page `header` to [boost our anchors](https://htmx.org/attributes/hx-boost/). |
| 68 | + |
| 69 | +## Create the rendering API |
| 70 | + |
| 71 | +Next we create our rendering API in an `api.py` module. We will place it at the root of our project (next to `main.py`), because we want this API to exist directly under the root `/` URL prefix. |
| 72 | + |
| 73 | +Note: `holm` applies file-system based routing to both pages and APIs! Actually, if both an API (`api.py`) and a page (`page.py`) exists in a directory, `holm` combines their routes into a single `APIRouter` for that path. API routes are registered first, followed by the page route at `GET` `/`. This means if your API defines a `GET` `/` route, it will conflict with your page route. |
| 74 | + |
| 75 | +The API we create will be very simple, it will have a single `/welcome-message` route that returns a welcome message in a randomly chosen language. The returned message is itself an `htmy` `Component`, so all we need is decorate the path operation function with `@htmy.hx()`. You can learn more about using `fasthx.htmy.HTMY` [here](https://volfpeter.github.io/fasthx/examples/htmy/). |
| 76 | + |
| 77 | +```python |
| 78 | +import random |
| 79 | + |
| 80 | +from fastapi import APIRouter |
| 81 | +from fasthx.htmy import HTMY |
| 82 | + |
| 83 | +_welcome_message: list[str] = [ |
| 84 | + "Welcome to My App! Powered By HTMX.", |
| 85 | + "Bienvenido a My App! Powered By HTMX.", |
| 86 | + "Bienvenue dans My App! Powered By HTMX.", |
| 87 | + "Willkommen bei My App! Powered By HTMX.", |
| 88 | + "Benvenuti nella mia app! Powered By HTMX.", |
| 89 | + "Üdvözöljük a My App-ban! Powered By HTMX.", |
| 90 | +] |
| 91 | + |
| 92 | + |
| 93 | +def api(htmy: HTMY) -> APIRouter: |
| 94 | + """Rendering API factories need an `htmy: fasthx.htmy.HTMY` argument.""" |
| 95 | + api = APIRouter() |
| 96 | + |
| 97 | + @api.get("/welcome-message") |
| 98 | + @htmy.hx() # type: ignore[arg-type] |
| 99 | + async def get_welcome_message() -> str: |
| 100 | + return random.choice(_welcome_message) # noqa: S311 |
| 101 | + |
| 102 | + return api |
| 103 | +``` |
| 104 | + |
| 105 | +Important details: |
| 106 | + |
| 107 | +- The `htmy.hx()` decorator is responsible for rendering the route's return value. It must be wrapped by the `@api` decorator. |
| 108 | +- Within the `api()` function, everything is standard FastAPI and FastHX functionality. |
| 109 | +- While this example only adds a rendering route, you can freely mix rendering and JSON APIs. |
| 110 | + |
| 111 | +## Add dynamic behavior to the home page |
| 112 | + |
| 113 | +Finally we add dynamic content to the home page (`page.py` in the root directory) with a couple of simple HTMX attributes. |
| 114 | + |
| 115 | +On the `h1` element, which contains our welcome message, we set: |
| 116 | + |
| 117 | +- `hx_get` to the use our newly created `/welcome-message` route when HTMX is triggered. |
| 118 | +- `hx_trigger` to `every 2s` to make the page load a new welcome message from our API every 2 seconds. |
| 119 | + |
| 120 | +These two attributes together will replace the displayed welcome message every two seconds without reloading the entire page. |
| 121 | + |
| 122 | +For the sake of completeness, we also wrap the link at the bottom in a `div` and use `hx_boost` as before to enhance the navigation experience. |
| 123 | + |
| 124 | +```python |
| 125 | +from htmy import Component, html |
| 126 | + |
| 127 | +# Static metadata for this page |
| 128 | +metadata = {"title": "Home | My App"} |
| 129 | + |
| 130 | + |
| 131 | +def page() -> Component: |
| 132 | + """Home page content.""" |
| 133 | + return html.div( |
| 134 | + html.h1( |
| 135 | + "Welcome to My App", |
| 136 | + hx_get="/welcome-message", |
| 137 | + hx_trigger="every 2s", |
| 138 | + ), |
| 139 | + html.p("This is a minimal holm application demonstrating:"), |
| 140 | + html.ul( |
| 141 | + html.li("File-system based routing"), |
| 142 | + html.li("Automatic layout composition"), |
| 143 | + html.li("Dynamic metadata"), |
| 144 | + html.li("Server-side rendering with htmy"), |
| 145 | + ), |
| 146 | + html.div( |
| 147 | + html.a("Learn more about us", href="/about"), |
| 148 | + hx_boost="true", # Explicit hx-boost for this link |
| 149 | + ), |
| 150 | + ) |
| 151 | +``` |
| 152 | + |
| 153 | +That's it! You can now run your application using `uvicorn` or `fastapi-cli`: |
| 154 | + |
| 155 | +```bash |
| 156 | +uvicorn main:app --reload |
| 157 | +``` |
| 158 | + |
| 159 | +Or with the FastAPI CLI if installed: |
| 160 | + |
| 161 | +```bash |
| 162 | +fastapi dev main.py |
| 163 | +``` |
| 164 | + |
| 165 | +You can now open your browser and navigate to `http://localhost:8000/` to see your application in action. |
| 166 | + |
| 167 | +If you followed every step correctly, then you will see the welcome message changing every two seconds on the home page. |
| 168 | + |
| 169 | +If you are curious to see how your API is called and how it responds, you can inspect requests on the Network tab of your browser's developer tools. |
0 commit comments