Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f2c04d1
chore: code restructuring
volfpeter Jul 23, 2025
d9c9307
chore: updating example apps + small doc fix
volfpeter Jul 23, 2025
93656f4
docs: update the Jinja intro in the readme
volfpeter Jul 23, 2025
af1a108
docs: update the htmy example in the documentation
volfpeter Jul 23, 2025
7f55448
docs: update the jinja example in the documentation
volfpeter Jul 23, 2025
8dc22f5
chore: fix type hint in custom-templating docs
volfpeter Jul 23, 2025
9e94407
docs: updated API documentation
volfpeter Jul 23, 2025
b7f7f18
chore: update dependencies
volfpeter Jul 23, 2025
27707c2
chore: bump patch version
volfpeter Jul 23, 2025
3ce19bd
chore: rename *HTMLRenderer types to *RenderFunction, refs #44
volfpeter Jul 23, 2025
c24184d
docs: add v3 upgrade guide, plus a minor doc fix
volfpeter Jul 23, 2025
626be7e
docs: add the v3 upgrade guide to the docs
volfpeter Jul 23, 2025
28a1a2e
coderabbit evaluation
volfpeter Jul 23, 2025
16af2fd
chore: fixed typos, removed comments that are no longer needed
volfpeter Jul 24, 2025
3790558
docs: replace remaining HTMLRenderer usage with RenderFunction
volfpeter Jul 24, 2025
764baac
docs: add Opinions section
volfpeter Aug 7, 2025
12745c0
docs: processing comments from coderabbit review
volfpeter Aug 7, 2025
5d8144d
docs: more nitpicking
volfpeter Aug 7, 2025
f00b676
chore: replace the stupid HTMY.htmy property with HTMY.renderer, refs
volfpeter Aug 11, 2025
f4d5a83
docs: fixing some small documentation issues
volfpeter Aug 11, 2025
3fbf95d
feat: upgrade htmy, and the htmy extra also brings lxml by default
volfpeter Aug 12, 2025
11c2432
feat: upgrade htmy to 0.8.1 and allow any RendererType implementations
volfpeter Aug 12, 2025
10f94f4
fix: v3 upgrade docs and htmy dev dependency version
volfpeter Aug 12, 2025
7bb3df0
chore: more intuitive (and shorter) poe task names
volfpeter Aug 14, 2025
24aee69
feat: simplify the RenderFunction type (require str return type) and
volfpeter Aug 14, 2025
fa8ff79
docs: nitpicking in the v3 upgrade docs
volfpeter Aug 14, 2025
6771ec4
chore: cleanup the page() core decorator
volfpeter Aug 14, 2025
1e5e6b5
chore: process typos based on coderabbit feedback
volfpeter Aug 14, 2025
6d821a4
feat: make the component_selector argument optional for the htmy
volfpeter Sep 26, 2025
9e14226
chore: add some type ignores where mypy can't infer the generic, not the
volfpeter Sep 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ FastAPI server-side rendering with built-in HTMX support.
- **Correct typing** makes it possible to apply other (typed) decorators to your routes.
- Works with both **sync** and **async routes**.

## Testimonials

"Thank you for your work on `fasthx`, as well as `htmy`! I've never had an easier time developing with another stack." ([ref](https://github.com/volfpeter/fasthx/discussions/77))

"One of the main parts of the `FastAPI` -> `fasthx` -> `htmy` integration I'm falling in love with is its explicitness, and not too much magic happening." ([ref](https://github.com/volfpeter/fasthx/issues/54))

"Thank you for your work on `htmy` and `fasthx`, both have been very pleasant to use, and the APIs are both intuitive and simple. Great work." ([ref](https://github.com/volfpeter/fasthx/issues/54))

## Support

Consider supporting the development and maintenance of the project through [sponsoring](https://buymeacoffee.com/volfpeter), or reach out for [consulting](https://www.volfp.com/contact?subject=Consulting%20-%20FastHX) so you can get the most out of the library.
Expand All @@ -41,10 +49,9 @@ The package has optional dependencies for the following **official integrations*

## Core concepts

The core concept of FastHX is to let FastAPI routes do their usual job of handling the business logic and returning the result, while the FastHX decorators take care
of the entire rendering / presentation layer using a declarative, decorator-based approach.
The core concept of FastHX is to let FastAPI routes do their usual job of handling the business logic and returning the result, while the FastHX decorators take care of the entire rendering and presentation layer using a declarative, decorator-based approach.

Interally, FastHX decorators always have access to the decorated route's result, all of its arguments (sometimes called the request context), and the current request. Integrations convert these values into data that can be consumed by the used rendering engine (for example `htmy` or `jinja`), run the rendering engine with the selected component (more on this below) and the created data, and return the result to the client. For more details on how data conversion works and how it can be customized, please see the API documentation of the rendering engine integration of your choice.
Internally, FastHX decorators always have access to the decorated route's result, all of its arguments (sometimes called the request context), and the current request. Integrations convert these values into data that can be consumed by the used rendering engine (for example `htmy` or `jinja`), run the rendering engine with the selected component (more on this below) and the created data, and return the result to the client. For more details on how data conversion works and how it can be customized, please see the API documentation of the rendering engine integration of your choice.

The `ComponentSelector` abstraction makes it possible to declaratively specify and dynamically select the component that should be used to render the response to a given request. It is also possible to define an "error" `ComponentSelector` that is used if the decorated route raises an exception -- a typical use-case being error rendering for incorrect user input.

Expand Down Expand Up @@ -98,12 +105,12 @@ def index() -> None: ...

Requires: `pip install fasthx[jinja]`.

To start serving HTML and HTMX requests, all you need to do is create an instance of `fasthx.Jinja` and use its `hx()` or `page()` methods as decorators on your routes. `hx()` only triggers HTML rendering for HTMX requests, while `page()` unconditionally renders HTML. See the example code below:
To start serving HTML and HTMX requests, all you need to do is create an instance of `fasthx.jinja.Jinja` and use its `hx()` or `page()` methods as decorators on your routes. `hx()` only triggers HTML rendering for HTMX requests, while `page()` unconditionally renders HTML. See the example code below:

```python
from fastapi import FastAPI
from fastapi.templating import Jinja2Templates
from fasthx import Jinja
from fasthx.jinja import Jinja
from pydantic import BaseModel

# Pydantic model of the data the example API is using.
Expand Down Expand Up @@ -145,7 +152,7 @@ See the full working example [here](https://github.com/volfpeter/fasthx/tree/mai

Requires: `pip install fasthx`.

If you would like to use a rendering engine without FastHX integration, you can easily build on the `hx()` and `page()` decorators which give you all the functionality you will need. All you need to do is implement the `HTMLRenderer` protocol.
If you would like to use a rendering engine without FastHX integration, you can easily build on the `hx()` and `page()` decorators which give you all the functionality you will need. All you need to do is implement the `RenderFunction` protocol.

Similarly to the Jinja case, `hx()` only triggers HTML rendering for HTMX requests, while `page()` unconditionally renders HTML. See the example code below:

Expand All @@ -158,7 +165,7 @@ from fasthx import hx, page
# Create the app.
app = FastAPI()

# Create a dependecy to see that its return value is available in the render function.
# Create a dependency to see that its return value is available in the render function.
def get_random_number() -> int:
return 4 # Chosen by fair dice roll.

Expand Down
9 changes: 1 addition & 8 deletions docs/api/core-decorators.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
# Core Decorators

## ::: fasthx.hx

options:
show_root_heading: true

## ::: fasthx.page
# ::: fasthx.core_decorators

options:
show_root_heading: true
9 changes: 1 addition & 8 deletions docs/api/dependencies.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
# FastAPI Dependencies

## ::: fasthx.DependsHXRequest

options:
show_root_heading: true

## ::: fasthx.get_hx_request
# ::: fasthx.dependencies

options:
show_root_heading: true
21 changes: 1 addition & 20 deletions docs/api/jinja.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,4 @@
# `Jinja`
# ::: fasthx.jinja

## ::: fasthx.Jinja
options:
show_root_heading: true

## ::: fasthx.JinjaContext
options:
show_root_heading: true

## ::: fasthx.TemplateHeader
options:
show_root_heading: true

## ::: fasthx.JinjaPath
options:
show_root_heading: true

## ::: fasthx.JinjaContextFactory
members:
- __call__
options:
show_root_heading: true
23 changes: 1 addition & 22 deletions docs/api/typing.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,4 @@
# Typing
# ::: fasthx.typing

## ::: fasthx.HTMLRenderer
options:
show_root_heading: true

## ::: fasthx.typing.SyncHTMLRenderer
members:
- __call__
options:
show_root_heading: true

## ::: fasthx.typing.AsyncHTMLRenderer
members:
- __call__
options:
show_root_heading: true

## ::: fasthx.typing.RequestComponentSelector
options:
show_root_heading: true

## ::: fasthx.typing.ComponentSelector
options:
show_root_heading: true
6 changes: 3 additions & 3 deletions docs/examples/custom-templating.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Custom Templating

If you're not into Jinja templating, the `hx()` and `page()` decorators give you all the flexibility you need: you can integrate any HTML rendering or templating engine into `fasthx` simply by implementing the `HTMLRenderer` protocol. Similarly to the Jinja case, `hx()` only triggers HTML rendering for HTMX requests, while `page()` unconditionally renders HTML. See the example code below:
If you're not into Jinja templating, the `hx()` and `page()` decorators give you all the flexibility you need: you can integrate any HTML rendering or templating engine into `fasthx` simply by implementing the `RenderFunction` protocol. Similarly to the Jinja case, `hx()` only triggers HTML rendering for HTMX requests, while `page()` unconditionally renders HTML. See the example code below:

```python
from typing import Annotated, Any
Expand All @@ -11,7 +11,7 @@ from fasthx import hx, page
# Create the app.
app = FastAPI()

# Create a dependecy to see that its return value is available in the render function.
# Create a dependency to see that its return value is available in the render function.
def get_random_number() -> int:
return 4 # Chosen by fair dice roll.

Expand All @@ -20,7 +20,7 @@ DependsRandomNumber = Annotated[int, Depends(get_random_number)]
# Create the render methods: they must always have these three arguments.
# If you're using static type checkers, the type hint of `result` must match
# the return type annotation of the route on which this render method is used.
def render_index(result: list[dict[str, str]], *, context: dict[str, Any], request: Request) -> str:
def render_index(result: Any, *, context: dict[str, Any], request: Request) -> str:
return "<h1>Hello FastHX</h1>"

def render_user_list(result: list[dict[str, str]], *, context: dict[str, Any], request: Request) -> str:
Expand Down
85 changes: 38 additions & 47 deletions docs/examples/htmy.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ First, let's create an `htmy_app.py` file, import everything that is required fo
import random
from dataclasses import dataclass
from datetime import date
from typing import Any

from fastapi import FastAPI
from fasthx.htmy import HTMY, ComponentHeader, CurrentRequest, RouteParams
from htmy import Component, Context, html
from pydantic import BaseModel

from fasthx.htmy import HTMY, ComponentHeader, CurrentRequest, RouteParams


class User(BaseModel):
"""User model."""
Expand All @@ -23,24 +23,19 @@ class User(BaseModel):
birthday: date
```

The main content on the user interface will be a user list, so let's start by creating a simple `UserListItem` component:
The main content on the user interface will be a user list, so let's start by creating a simple `user_list_item` component factory (see the [htmy components guide](https://volfpeter.github.io/htmy/components-guide/) for more information):

```python
@dataclass
class UserListItem:
"""User list item component."""

user: User

def htmy(self, context: Context) -> Component:
return html.li(
html.span(self.user.name, class_="font-semibold"),
html.em(f" (born {self.user.birthday.isoformat()})"),
class_="text-lg",
)
def user_list_item(user: User) -> html.li:
"""User list item component factory."""
return html.li(
html.span(user.name, class_="font-semibold"),
html.em(f" (born {user.birthday.isoformat()})"),
class_="text-lg",
)
```

As you can see, the component has a single `user` property and it renders an `<li>` HTML element with the user's name and birthday in it.
As you can see, the component factory expects a `user` and it creates an `<li>` HTML element with the user's name and birthday in it.

The next component we need is the user list itself. This is going to be the most complex part of the example:

Expand Down Expand Up @@ -70,7 +65,7 @@ class UserOverview:
rerenders: int = route_params["rerenders"]

# Create the user list item generator.
user_list_items = (UserListItem(u) for u in self.users)
user_list_items = (user_list_item(u) for u in self.users)

# Create the ordered or unordered user list.
user_list = (
Expand All @@ -92,7 +87,7 @@ class UserOverview:
user_list,
# -- HTMX directives.
hx_trigger="load delay:1000",
hx_get=f"/users?rerenders={rerenders+1}",
hx_get=f"/users?rerenders={rerenders + 1}",
hx_swap="outerHTML",
# Send the next component variant in an X-Component header.
hx_headers=f'{{"X-Component": "{next_variant}"}}',
Expand All @@ -107,37 +102,33 @@ Most of this code is basic Python and `htmy` usage (including the `hx_*` `HTMX`
- The use of `RouteParams.from_context()` to get access to every route parameter (resolved FastAPI dependency) as a mapping.
- The `context["user-agent"]` lookup that accesses a value from the context which will be added by a _request processor_ later in the example.

We need one last `htmy` component, the index page. Most of this component is just the basic HTML document structure with some TailwindCSS styling and metadata. There is also a bit of `HTMX` in the `body` for lazy loading the actual page content, the user list we just created.
We need one last `htmy` component, the index page. Most of this component (component factory to be more precise) is just the basic HTML document structure with some TailwindCSS styling and metadata. There is also a bit of `HTMX` in the `body` for lazy loading the actual page content, the user list we just created.

```python
@dataclass
class IndexPage:
"""Index page with TailwindCSS styling."""

def htmy(self, context: Context) -> Component:
return (
html.DOCTYPE.html,
html.html(
html.head(
# Some metadata
html.title("FastHX + HTMY example"),
html.meta.charset(),
html.meta.viewport(),
# TailwindCSS
html.script(src="https://cdn.tailwindcss.com"),
# HTMX
html.script(src="https://unpkg.com/[email protected]"),
),
html.body(
# Page content: lazy-loaded user list.
html.div(hx_get="/users", hx_trigger="load", hx_swap="outerHTML"),
class_=(
"h-screen w-screen flex flex-col items-center justify-center "
" gap-4 bg-slate-800 text-white"
),
def index_page(_: Any) -> Component:
return (
html.DOCTYPE.html,
html.html(
html.head(
# Some metadata
html.title("FastHX + HTMY example"),
html.meta.charset(),
html.meta.viewport(),
# TailwindCSS
html.script(src="https://cdn.tailwindcss.com"),
# HTMX
html.script(src="https://unpkg.com/[email protected]"),
),
html.body(
# Page content: lazy-loaded user list.
html.div(hx_get="/users", hx_trigger="load", hx_swap="outerHTML"),
class_=(
"h-screen w-screen flex flex-col items-center justify-center "
" gap-4 bg-slate-800 text-white"
),
),
)
),
)
```

With all the components ready, we can now create the `FastAPI` and `fasthx.htmy.HTMY` instances:
Expand All @@ -159,11 +150,11 @@ Note how we added a _request processor_ function to the `HTMY` instance that tak

All that remains now is the routing. We need two routes: one that serves the index page, and one that renders the ordered or unordered user list.

The index page route is trivial. The `htmy.page()` decorator expects a component factory (well more precisely a `fasthx.ComponentSelector`) that accepts the route's return value and returns an `htmy` component. Since `IndexPage` has no properties, we use a simple `lambda` to create such a function:
The index page route is trivial. The `htmy.page()` decorator expects a component factory (well more precisely a `fasthx.ComponentSelector`) that accepts the route's return value and returns an `htmy` component. `index_page` is implemented exactly like this, so we can use it directly in the decorator:

```python
@app.get("/")
@htmy.page(lambda _: IndexPage())
@htmy.page(index_page)
def index() -> None:
"""The index page of the application."""
...
Expand Down
11 changes: 5 additions & 6 deletions docs/examples/jinja-templating.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

## Basics

To start serving HTML and HTMX requests, all you need to do is create an instance of `fasthx.Jinja` and use its `hx()` or `page()` methods as decorators on your routes. `hx()` only triggers HTML rendering for HTMX requests, while `page()` unconditionally renders HTML, saving you some boilerplate code. See the example code below:
To start serving HTML and HTMX requests, all you need to do is create an instance of `fasthx.jinja.Jinja` and use its `hx()` or `page()` methods as decorators on your routes. `hx()` only triggers HTML rendering for HTMX requests, while `page()` unconditionally renders HTML, saving you some boilerplate code. See the example code below:

```python
from fastapi import FastAPI
from fastapi.templating import Jinja2Templates
from fasthx import Jinja
from fasthx.jinja import ComponentHeader, Jinja
from pydantic import BaseModel

# Pydantic model of the data the example API is using.
Expand Down Expand Up @@ -43,10 +43,9 @@ def htmx_only() -> list[User]:
return [User(first_name="Billy", last_name="Shears")]
```

## Using `TemplateHeader`
## Using `ComponentHeader`

In the basic example, routes always rendered a fixed HTML template. `TemplateHeader` lifts this restriction by letting the client submit the _key_ of the required template,
automatically looking up the corresponding template, and of course rendering it.
In the basic example, routes always rendered a fixed HTML template. `ComponentHeader` lifts this restriction by letting the client submit the _key_ of the required template, automatically looking up the corresponding template, and of course rendering it.

This can be particularly helpful when multiple templates/UI components require the same data and business logic.

Expand All @@ -56,7 +55,7 @@ jinja = Jinja(Jinja2Templates("templates"))

@app.get("/profile/{id}")
@jinja.hx(
TemplateHeader(
ComponentHeader(
"X-Component",
{
"card": "profile/card.jinja",
Expand Down
Loading