Package: fastapi-htmx
FastAPI-HTMXExtension for FastAPI to make HTMX easier to use.FastAPI-HTMX is an opinionated extension for FastAPI to speed up development of lightly interactive web applications. FastAPI-HTMX is implemented as a decorator, so it can be used on endpoints selectively. Furthermore it reduces boilerplate for Jinja2 template handling and allows for rapid prototyping by providing convenient helpers.Installinstall viapip:$ pip install fastapi-htmxinstall viapoetry:$ poetry add fastapi-htmxUsageGetting StartedBasic example using FastAPI withfastapi-htmxmy_app/api.py:frompathlibimportPathfromfastapiimportFastAPI,Requestfromfastapi.responsesimportHTMLResponsefromfastapi.templatingimportJinja2Templatesfromfastapi_htmximporthtmx,htmx_initapp=FastAPI()htmx_init(templates=Jinja2Templates(directory=Path("my_app")/"templates"))@app.get("/",response_class=HTMLResponse)@htmx("index","index")asyncdefroot_page(request:Request):return{"greeting":"Hello World"}@app.get("/customers",response_class=HTMLResponse)@htmx("customers")asyncdefget_customers(request:Request):return{"customers":["John Doe","Jane Doe"]}Note that:htmx()got parameters, specifying the Jinja2 template to usehtmx_init()is needed for FastAPI-HTMX to find the templatesThere is no direct handling of the template needed, it only needs to be specified and the needed variables need to be returned. This way endpoints can be designed in a familiar way to standard REST endpoints in FastAPI.This simplifies modularizing the app later (see below) and also providing a REST API if needed. See the "Usage" section for further examples.get_customersdoes not respond with the whole web page, but only with a part of it. See theHTMX documentationon how HTMX merges partials into the current web page.request: Requestalthough not used in the endpoint directly, it is currently required for the decorator to work!TheJinja2 templatesto go along with the above code need to be placed like specified inhtmx_initinmy_app/templates/in order for the example to work.The root pagemy_app/templates/index.jinja2:<!DOCTYPE html>
<html>
<head>
    <title>Hello FastAPI-HTMX</title>
</head>
<body>
    <h1>{{ greeting }}</h1>
    <button
        hx-get="/customers"
        hx-swap="innerHTML"
        hx-target="#customers_list"
    >
        Load Data
    </button>
    <div id="customers_list"></div>
    <script src="https://unpkg.com/htmx.org@1.9.6"></script>
</body>
</html>Thepartial template to load with HTMXmy_app/templates/customers.jinja2:<ul>
    {% for customer in customers %}
        <li>{{ customer }}</li>
    {% endfor %}
</ul>Main ConceptThe decoratorhtmxprovides the following helpers:partial_template_nameThe partial template to usefull_template_nameThe full page template to use when URL rewriting + history is used*_template_constructorFor DRY code, in case the logic to gather all needed variables is needed multiple timesSeeing these arguments one might ask themselves: Why all these parameters? The answer is an opinionated take on how to design modular endpoints wit partials and url-rewriting support:The idea behind FastAPI-HTMX is to maintain a modular structure in the app and with the endpoints. Similar to a REST API with aSPA. This way the frontend can be modular as well. This majorly helps with supportingURL rewriting and the historyin the frontend:A simple endpoint just answers with the partial.Without it, if the URL is rewritten and a user navigates back, reloads the page or copies the URL and opens it in another tab or shares the URL, only the partial would be shown in the browser.To enable SPA like functionality FastAPI-HTMX uses the concept of partials and fullpages as arguments for the decorator and requires to return a dict of the needed variables. Note that returning anything else than aMappinglike a dict in the route, leads FastAPI-HTMX to return that instead of a template.In order to support this SPA like functionality in an app, see the following example:my_app/api_with_constructors.py:frompathlibimportPathfromfastapiimportFastAPI,Requestfromfastapi.responsesimportHTMLResponsefromfastapi.templatingimportJinja2Templatesfromfastapi_htmximporthtmx,htmx_initapp=FastAPI()htmx_init(templates=Jinja2Templates(directory=Path("my_app")/"templates"))defconstruct_customers():return{"customers":["John Doe","Jane Doe"]}defconstruct_root_page():return{"greeting":"Hello World",**construct_customers()}@app.get("/",response_class=HTMLResponse)@htmx("index","index")asyncdefroot_page(request:Request):returnconstruct_root_page()@app.get("/customers",response_class=HTMLResponse)@htmx("customers","index",construct_customers,construct_root_page)asyncdefget_customers(request:Request):passNote that:Theconstruct_*functions are added, they now return the dataconstruct_root_pagegathers all variables specified needed for the root page, including those for partialsThis also means you must avoid naming conflicts across endpoints, so dicts can be merged.Costly operations can still be ignored, just use if statements in the template or similarThe decorators arguments are extendedThe second argument is the fullpage template which is used when the endpoint is called directly (new tab, navigation or reload)E.g. sinceconstruct_root_pagegathers all the data for the whole page, the whole page can be returned to the clientThe other arguments are just to save some boilerplate code handling theHX-RequestheaderThere is no need to use the arguments for the constructor functions, they are just for convenience.If needed the endpoint can be used for the logic as well. Especially if no URL rewriting is needed.For the above code to work themy_app/templates/index.jinja2needs to be changed as well. The changes are in the button and target div.
Changed root pagemy_app/templates/index.jinja2:<!DOCTYPE html>
<html>
<head>
    <title>Hello FastAPI-HTMX</title>
</head>
<body>
    <h1>{{ greeting }}</h1>
    <button
        hx-get="/customers"
        hx-push-url="true"
        hx-swap="innerHTML"
        hx-target="#customers_list"
    >
        Load Data
    </button>
    <div id="customers_list">
        {% include 'customers.jinja2' %}
    </div>
    <script src="https://unpkg.com/htmx.org@1.9.6"></script>
</body>
</html>Note that:hx-push-url="true"was added to the buttonThe partial is now loaded by default requiring the main endpoint to also provide the needed variables like shown aboveThe unchanged partialmy_app/templates/customers.jinja2:<ul>
    {% for customer in customers %}
        <li>{{ customer }}</li>
    {% endfor %}
</ul>To add additional partials and endpoints just repeat the same logic:Include the partial in the parent Jinja2 template, like the main template. A hierarchy is possible as well.Refactor the partials endpoints logic into a functionAdd it's return value to the parents constructor function like done above inconstruct_root_pageAdd the parents template and constructor function to the partials endpointshtmxdecorator argumentsAdvanced UsageHandlingHX-RequestmanuallyIn case thehtmx()arguments for partial and fullpage callables are not flexible enough, an endpoint can be used like usual. For a bit more convenience theHX-Requestheader is easily accessible viarequest.hx_request:my_app/api_with_hx_request.py:frompathlibimportPathfromfastapiimportFastAPIfromfastapi.responsesimportHTMLResponsefromfastapi.templatingimportJinja2Templatesfromfastapi_htmximportHXRequest,htmx,htmx_initapp=FastAPI()htmx_init(templates=Jinja2Templates(directory=Path("my_app")/"templates"))defmy_partial(email_id:int=None):return{"email_id":email_id}deffullpage(email_id:int=None):return{**my_partial(email_id)}@app.get("/email/{email_id}",response_class=HTMLResponse)@htmx("email_detail","index")defget_email(request:HXRequest,email_id:int):ifrequest.hx_request:returnmy_partial(email_id)else:returnfullpage(email_id)my_app/templates/index.jinja2:<!DOCTYPE html>
<html><head><title>Hello FastAPI-HTMX</title></head>
<body>
    <div id="email_detail">
        {% include 'email_detail.jinja2' %}
    </div>
    <script src="https://unpkg.com/htmx.org@1.9.6"></script>
</body>
</html>my_app/templates/email_detail.jinja2:<p>{{ email_id }}</p>FiltersIn order to usecustom Jinja2 filterslike the following, configure them like below.my_app/templates/customer.jinja2:<p>{{ customer.created|datetime_format }}</p>Add custom filters for use in Jinja2 templates:my_app/api_template_filters.py:frompathlibimportPathfromdatetimeimportdatetimefromfastapi.templatingimportJinja2Templatesfromfastapi_htmximporthtmx_initdefdatetime_format(value:datetime,format="%H:%M%d.%m.%Y"):returnvalue.strftime(format)ifvalueisnotNoneelse""templates=Jinja2Templates(directory=Path("my_app")/"templates")templates.env.filters["datetime_format"]=datetime_formathtmx_init(templates=templates)# ...Multiple Template DirectoriesFor bigger apps, multiple template directories might be needed. For example if the app is split into several modules connected with routers. For this case template collections can be used. Withhtmx_init()multiple template collections can be defined. In each endpoint instead of the templates name (account) the collection gets specified as well, like this:Tpl(SHOP, "account"). This works for partials and full pages.my_app/api_multiple_template_paths.py:frompathlibimportPathfromfastapiimportFastAPI,Requestfromfastapi.responsesimportHTMLResponsefromfastapi.templatingimportJinja2Templatesfromfastapi_htmximporthtmx,htmx_init,TemplateSpecasTplSHOP="shop"BACKEND="backend"app=FastAPI()templates={SHOP:Jinja2Templates(directory=Path("my_app")/SHOP/"templates"),BACKEND:Jinja2Templates(directory=Path("my_app")/BACKEND/"templates")}htmx_init(templates=templates)@app.get("/account",response_class=HTMLResponse)@htmx(Tpl(SHOP,"account"))asyncdefget_account(request:Request):pass@app.get("/products",response_class=HTMLResponse)@htmx(Tpl(BACKEND,"products"))asyncdefget_products(request:Request):passmy_app/shop/templates/account.jinja2:<h1>My Account</h1>my_app/backend/templates/products.jinja2:<h1>Products</h1>Other template file extensions for SOME endpointsIn case SOME endpoints templates got another file extension than the rest, it can be specified in the@htmx()decorator:my_app/api_template_file_extension.py:frompathlibimportPathfromfastapiimportFastAPI,Requestfromfastapi.responsesimportHTMLResponsefromfastapi.templatingimportJinja2Templatesfromfastapi_htmximporthtmx,htmx_initapp=FastAPI()htmx_init(templates=Jinja2Templates(directory=Path("my_app")/"templates"))@app.get("/customers",response_class=HTMLResponse)@htmx("customers",template_extension="html")asyncdefget_customers(request:Request):pass# ...my_app/templates/customers.html:<h1>Customer</h1>Other template file extensions for ALL endpointsIn case ALL endpoints templates got another file extension than the defaultjinja2, it can be overriden inhtmx_init()like this:my_app/api_template_file_extension.py:frompathlibimportPathfromfastapiimportFastAPI,Requestfromfastapi.responsesimportHTMLResponsefromfastapi.templatingimportJinja2Templatesfromfastapi_htmximporthtmx,htmx_initapp=FastAPI()templates=Jinja2Templates(directory=Path("my_app")/"templates")htmx_init(templates=templates,file_extension="html")@app.get("/customers",response_class=HTMLResponse)@htmx("customers")asyncdefget_customers(request:Request):passmy_app/templates/customers.html:<h1>Customer</h1>