Skip to content

Commit a3db636

Browse files
authored
Merge pull request #3 from volfpeter/feat/example-rendering-api-with-htmx
Rendering APIs with HTMX guide + doc improvements + FastHX upgrade
2 parents 61c3bd5 + 9b9abf6 commit a3db636

File tree

15 files changed

+316
-14
lines changed

15 files changed

+316
-14
lines changed

README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ Web development framework that brings the Next.js developer experience to Python
1515

1616
- **Next.js**-like **developer experience** with **file-system based routing** and page composition.
1717
- **Standard FastAPI** everywhere, so you can leverage the entire FastAPI ecosystem.
18+
- **JSX-like syntax** with async support for components, thanks to `htmy`.
19+
- First class **HTMX support** with `FastHX`.
20+
- **Async** support everywhere, from APIs and dependencies all the way to UI components.
21+
- Support for both **JSON** and **HTML** (server side rendering) APIs.
1822
- **No JavaScript** dependencies
1923
- **No build steps**, just server side rendering with **fully typed Python**.
20-
- **Async** support everywhere, from APIs and dependencies all the way to UI components, thanks to `htmy`.
21-
- Support for both **JSON** and **HTML** (server side rendering) APIs.
22-
- First class `HTMX` support thanks to `FastHX`.
2324
- **Stability** by building only on the core feature set of dependent libraries.
2425
- **Unopinionated**: use any CSS framework for styling and any JavaScript framework for UI interactivity.
2526

@@ -71,7 +72,9 @@ All you need to do is follow these simple rules:
7172

7273
## Examples
7374

74-
If you prefer to learn through examples, the [Quick start guide](https://volfpeter.github.io/holm/quick-start-guide) is the best place to start. The entire source code of the quick start guide application can be found in the [examples/quick-start-guide](https://github.com/volfpeter/holm/tree/main/examples/quick-start-guide) directory of the repository.
75+
If you prefer to learn through examples, the [Quick start guide](https://volfpeter.github.io/holm/quick-start-guide) is the best place to start.
76+
77+
To learn about creating rendering APIs and adding HTMX to your application, you should have a look at the [Rendering APIs with HTMX](https://volfpeter.github.io/holm/rendering-apis-with-htmx) guide.
7578

7679
You can discover even more features by exploring the [test application](https://github.com/volfpeter/holm/tree/main/test_app) of the project.
7780

docs/application-components.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ These are standard FastAPI `APIRouter`s, but you should still follow these recom
4242

4343
- If routes in the API don't do any rendering, then the `api()` callable should have no arguments or simply an `api` variable should be used. Otherwise the `api()` callable should have a single `fasthx.htmy.HTMY` positional argument. The application's renderer will be passed to this function automatically by `holm`.
4444
- If an `api()` callable is used, it may have further arguments as long as they all have default values. This pattern can simplify API testing for example, by allowing custom configurations for tests.
45-
- The `APIRouter` should not have a `prefix`. The application's URL structure automatically matches the package structure.
4645

4746
Note on rendering APIs:
4847

docs/index.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ Web development framework that brings the Next.js developer experience to Python
66

77
- **Next.js**-like **developer experience** with **file-system based routing** and page composition.
88
- **Standard FastAPI** everywhere, so you can leverage the entire FastAPI ecosystem.
9+
- **JSX-like syntax** with async support for components, thanks to `htmy`.
10+
- First class **HTMX support** with `FastHX`.
11+
- **Async** support everywhere, from APIs and dependencies all the way to UI components.
12+
- Support for both **JSON** and **HTML** (server side rendering) APIs.
913
- **No JavaScript** dependencies
1014
- **No build steps**, just server side rendering with **fully typed Python**.
11-
- **Async** support everywhere, from APIs and dependencies all the way to UI components, thanks to `htmy`.
12-
- Support for both **JSON** and **HTML** (server side rendering) APIs.
13-
- First class `HTMX` support thanks to `FastHX`.
1415
- **Stability** by building only on the core feature set of dependent libraries.
1516
- **Unopinionated**: use any CSS framework for styling and any JavaScript framework for UI interactivity.
1617

@@ -62,7 +63,9 @@ All you need to do is follow these simple rules:
6263

6364
## Examples
6465

65-
If you prefer to learn through examples, the [Quick start guide](quick-start-guide.md) is the best place to start. The entire source code of the quick start guide application can be found in the [examples/quick-start-guide](https://github.com/volfpeter/holm/tree/main/examples/quick-start-guide) directory of the repository.
66+
If you prefer to learn through examples, the [Quick start guide](quick-start-guide.md) is the best place to start.
67+
68+
To learn about creating rendering APIs and adding HTMX to your application, you should have a look at the [Rendering APIs with HTMX](rendering-apis-with-htmx.md) guide.
6669

6770
You can discover even more features by exploring the [test application](https://github.com/volfpeter/holm/tree/main/test_app) of the project.
6871

docs/quick-start-guide.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ This example will get you up and running quickly by demonstrating the following
1010
- FastAPI integration: Use standard FastAPI features (dependencies) in layouts, pages, and metadata functions.
1111
- htmy components: Build your UI in typed Python with a JSX-like syntax.
1212

13+
The entire source code of this application can be found in the [examples/quick-start-guide](https://github.com/volfpeter/holm/tree/main/examples/quick-start-guide) directory of the repository.
14+
1315
## Create the application structure
1416

1517
Before starting this guide, make sure you have installed `holm` and either `uvicorn` or `fastapi-cli` with `pip` (`pip install holm uvicorn` or `pip install holm fastapi-cli`)!
@@ -148,13 +150,13 @@ That's it! You now have a working application. From here, you can add more pages
148150

149151
## Run your application
150152

151-
You can now run your application using `uvicorn` or `fastapi-cli` :
153+
You can now run your application using `uvicorn` or `fastapi-cli`:
152154

153155
```bash
154156
uvicorn main:app --reload
155157
```
156158

157-
Or with FastAPI CLI if installed:
159+
Or with the FastAPI CLI if installed:
158160

159161
```bash
160162
fastapi dev main.py

docs/rendering-apis-with-htmx.md

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The quick start guide example with a rendering APIs with HTMX guide's changes.

examples/rendering-apis-with-htmx/about/__init__.py

Whitespace-only changes.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from htmy import Component, html
2+
3+
4+
async def metadata(featured: bool = False) -> dict[str, str]:
5+
"""
6+
Dynamic metadata based on query parameters.
7+
8+
This function could be both sync or async. It's just a standard FastAPI dependency.
9+
"""
10+
title = "Featured About" if featured else "About"
11+
return {"title": f"{title} | My App"}
12+
13+
14+
async def page(featured: bool = False) -> Component:
15+
"""Async about page with dynamic content."""
16+
if featured:
17+
return html.div(
18+
html.h1("About Us ⭐"),
19+
html.p("This is our featured about page!"),
20+
html.p("You're viewing the special featured version."),
21+
html.a("Regular version", href="/about"),
22+
)
23+
24+
return html.div(
25+
html.h1("About Us"),
26+
html.p("We're building amazing web applications with holm."),
27+
html.p("Our framework combines the power of FastAPI with server-side rendering."),
28+
html.a("Featured version", href="/about?featured=true"),
29+
)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import random
2+
3+
from fastapi import APIRouter
4+
from fasthx.htmy import HTMY
5+
6+
_welcome_message: list[str] = [
7+
"Welcome to My App! Powered By HTMX.",
8+
"Bienvenido a My App! Powered By HTMX.",
9+
"Bienvenue dans My App! Powered By HTMX.",
10+
"Willkommen bei My App! Powered By HTMX.",
11+
"Benvenuti nella mia app! Powered By HTMX.",
12+
"Üdvözöljük a My App-ban! Powered By HTMX.",
13+
]
14+
15+
16+
def api(htmy: HTMY) -> APIRouter:
17+
"""Rendering API factories need an `htmy: fasthx.htmy.HTMY` argument."""
18+
api = APIRouter()
19+
20+
@api.get("/welcome-message")
21+
@htmy.hx() # type: ignore[arg-type]
22+
async def get_welcome_message() -> str:
23+
return random.choice(_welcome_message) # noqa: S311
24+
25+
return api
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from htmy import Component, ComponentType, Context, component, html
2+
3+
from holm import Metadata
4+
5+
6+
@component
7+
def layout(children: ComponentType, context: Context) -> Component:
8+
"""Root layout wrapping all pages."""
9+
metadata = Metadata.from_context(context)
10+
11+
return (
12+
html.DOCTYPE.html,
13+
html.html(
14+
html.head(
15+
html.title(metadata.get("title", "My App")),
16+
html.meta(charset="utf-8"),
17+
html.meta(name="viewport", content="width=device-width, initial-scale=1"),
18+
html.link( # Use PicoCSS to add some default styling.
19+
rel="stylesheet", href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
20+
),
21+
html.script(src="https://unpkg.com/htmx.org@2.0.7"),
22+
),
23+
html.body(
24+
html.header(
25+
html.nav(
26+
html.ul(
27+
html.li(html.a("Home", href="/")),
28+
html.li(html.a("About", href="/about")),
29+
),
30+
hx_boost="true",
31+
),
32+
class_="container",
33+
),
34+
html.main(children, class_="container"),
35+
html.footer(html.p("© 2025 My App"), class_="container"),
36+
class_="container-fluid",
37+
),
38+
),
39+
)

0 commit comments

Comments
 (0)