FastHX

repository·main·Indexed 20 days ago

https://github.com/volfpeter/fasthx

A Python server-side rendering library for FastAPI with built-in HTMX support. FastHX uses a decorator-based approach to separate business logic from the presentation layer, offering integrations for htmy and Jinja2 templating engines, as well as a RenderFunction protocol for custom rendering implementations. It supports progressive HTML streaming via StreamingRenderer and provides utilities like ComponentSelector and ComponentHeader for dynamic component selection.

Tokens
13.5K
Snippets
41
Records
57
Agent score
72%

What's inside fasthx

  1. Key features of FastHX

    main

    FastHX is a Python server-side rendering library for FastAPI designed for HTMX-based applications. Key capabilities include:

    • FastAPI Integration: Uses standard Python decorator syntax without magic dependencies in routes.
    • Templating Agnostic: Works with htmy, jinja2, dominate, or any other SSR library.
    • HTMX Optimized: Built for HTMX but usable standalone. Routes work correctly even if they receive non-HTMX requests (serving both data and HTML).
    • Async HTML Streaming: Supports async streaming for better performance (TTFB/FCP).
    • Context Awareness: The rendering engine has access to all route dependencies and the current request.
    • Header Preservation: Response headers set in your routes are preserved after rendering.
    • Type Safety: Correct typing allows for applying other typed decorators to your routes.
    • Sync/Async Support: Works with both synchronous and asynchronous routes.
  2. Access dependencies in custom render functions

    main

    When using custom render functions with @hx() or @page(), any FastAPI dependencies injected into the route function are made available in the context dictionary of the render function. The key in the context dictionary matches the name of the dependency parameter used in the route function.

    from typing import Annotated, Any
    from fastapi import Depends, FastAPI, Request
    from fasthx import hx
    
    app = FastAPI()
    
    def get_random_number() -> int:
        return 4
    
    # Dependency type
    DependsRandomNumber = Annotated[int, Depends(get_random_number)]
    
    def render_user_list(result: list[dict[str, str]], *, context: dict[str, Any], request: Request) -> str:
        # The dependency 'random_number' is accessible via the context dict
        random_number = context["random_number"]
        return f"<h1>{random_number}</h1>"
    
    @app.get("/htmx-or-data")
    @hx(render_user_list)
    def htmx_or_data(random_number: DependsRandomNumber) -> list[dict[str, str]]:
        return [{"name": "Joe"}]
  3. Core concepts of FastHX

    main

    FastHX uses a declarative, decorator-based approach to separate business logic from the presentation layer.

    How it works

    FastAPI routes handle business logic and return results. FastHX decorators then intercept these results, the route's arguments (the request context), and the current request. An integration (like htmy or jinja) converts these values into data for the rendering engine, executes the engine with a selected component, and returns the rendered result to the client.

    ComponentSelector

    The ComponentSelector abstraction allows you to declaratively specify and dynamically select which component should render the response for a given request. You can also define an "error" ComponentSelector to handle cases where the decorated route raises an exception (e.g., for rendering error states due to invalid user input).

  4. How FastHX and rendering engines work together

    main

    FastHX uses a decorator-based approach to separate business logic from the presentation layer.

    1. FastAPI Routes: Handle business logic and return data (e.g., Pydantic models, lists, or dicts).
    2. FastHX Decorators: Intercept the route's result, arguments (request context), and the current request.
    3. Integrations: Convert these values into data for the rendering engine (like htmy or jinja).
    4. Rendering: The engine runs the selected component/template with the data and returns the HTML to the client.

    Key abstractions include:

    • ComponentSelector: Allows you to declaratively specify and dynamically select which component renders the response. You can also define an "error" selector for handling exceptions.
  5. Configure Request Processors in HTMY

    main

    You can extend the htmy rendering context by providing request_processors to the HTMY constructor. A request processor is a function that takes a fastapi.Request and returns a dictionary of values. These values are merged into the htmy context and made available to all components during rendering.

    This is useful for injecting global request metadata like headers or user information into your component logic.

    from fastapi import FastAPI
    from fasthx.htmy import HTMY
    
    app = FastAPI()
    
    htmy = HTMY(
        # This processor adds 'user-agent' to the context for all components
        request_processors=[
            lambda request: {"user-agent": request.headers.get("user-agent")},
        ]
    )
  6. Use Jinja2 templating with FastHX

    main

    To use Jinja2, install pip install fasthx[jinja]. Wrap a standard FastAPI Jinja2Templates instance in a fasthx.jinja.Jinja instance.

    • @jinja.hx("template.html"): Triggers HTML rendering only for HTMX requests. For non-HTMX requests, it returns the raw route result (e.g., JSON).
    • @jinja.page("template.html"): Unconditionally renders HTML for all requests.
    • @jinja.hx("template.html", no_data=True): Renders the template but does not pass the route's return value as data to the template.
    from fastapi import FastAPI
    from fastapi.templating import Jinja2Templates
    from fasthx.jinja import Jinja
    
    app = FastAPI()
    # Wrap Jinja2Templates in FastHX Jinja
    jinja = Jinja(Jinja2Templates("templates"))
    
    @app.get("/")
    @jinja.page("index.html")
    def index() -> None:
        ...
    
    @app.get("/user-list")
    @jinja.hx("user-list.html")
    async def htmx_or_data() -> list[User]:
        return [User(first_name="John", last_name="Lennon")]
  7. Migrate htmy integration to version 3

    main

    When upgrading to fasthx v3, ensure your environment meets the following requirements and update your code as follows:

    1. Dependency Update: Ensure htmy version is >=0.8.1.
    2. Property Rename: The HTMY instance no longer uses the htmy property. Use the renderer property instead.
  8. Migrate custom RequestComponentSelector implementations to v2

    main

    In version 2, RequestComponentSelector was updated to support exception rendering. If you have custom implementations, you must update the method signature and logic:

    1. Rename Method: Change get_component_id() to get_component().
    2. Update Signature: Add a new argument error: Exception | None to the method.
    3. Handle Errors: If your selector does not support error rendering, it should reraise the received error if it is not None. While not strictly required for the core decorators to function (as results and errors are separated in Jinja), reraising is considered best practice for well-behaved selectors.
    # Example of updated signature in v2
    def get_component(self, ..., error: Exception | None = None) -> ...:
        if error is not None:
            raise error
        # ... normal logic
  9. Use Jinja templating with FastHX

    main

    To serve HTML and HTMX requests using Jinja2, create an instance of fasthx.jinja.Jinja by passing a fastapi.templating.Jinja2Templates instance to its constructor. You can then use the @jinja.hx() or @jinja.page() decorators on your FastAPI routes.

    • @jinja.hx(template_name): Only triggers HTML rendering if the incoming request is an HTMX request. For non-HTMX requests, it returns the raw data (e.g., JSON).
    • @jinja.page(template_name): Unconditionally renders the specified HTML template, regardless of whether the request is from HTMX or not.
    from fastapi import FastAPI
    from fastapi.templating import Jinja2Templates
    from fasthx.jinja import Jinja
    
    app = FastAPI()
    # Initialize Jinja with your FastAPI Jinja2Templates instance
    jinja = Jinja(Jinja2Templates("templates"))
    
    @app.get("/")
    @jinja.page("index.html")
    def index() -> None:
        ...
    
    @app.get("/user-list")
    @jinja.hx("user-list.html")
    async def htmx_or_data() -> list[dict]:
        return [{"first_name": "John", "last_name": "Lennon"}]
  10. Migrate Jinja integration to fasthx.jinja

    main

    In version 3, all Jinja-related utilities have been moved to the fasthx.jinja package. You must update your imports to use this new location. Additionally, TemplateHeader has been renamed to ComponentHeader.

    Import Changes:

    • from fasthx import Jinja $\rightarrow$ from fasthx.jinja import Jinja
    • from fasthx import JinjaContext $\rightarrow$ from fasthx.jinja import JinjaContext
    • from fasthx import JinjaPath $\rightarrow$ from fasthx.jinja import JinjaPath
    • from fasthx import JinjaContextFactory $\rightarrow$ from fasthx.jinja import JinjaContextFactory
    • from fasthx.typing import JinjaContextFactory $\rightarrow$ from fasthx.jinja import JinjaContextFactory
    • from fasthx import TemplateHeader $\rightarrow$ from fasthx.jinja import ComponentHeader
    • from fasthx.jinja import TemplateHeader $\rightarrow$ from fasthx.jinja import ComponentHeader

    API Changes:

    • If you used the templates keyword argument in TemplateHeader(), you must now use the components argument in ComponentHeader().
    • If you have overridden Jinja._make_response(), you must now override Jinja._make_render_function() instead.
  11. Install FastHX

    main

    Install the core FastHX package via pip:

    $ pip install fasthx

    FastHX has optional dependencies for official integrations. Use the following commands to install them:

    • htmy integration: pip install fasthx[htmy]
    • jinja integration: pip install fasthx[jinja]