Working with FastAPI
FastAPI is a modern, high-performance Python web framework. Jx integrates cleanly with FastAPI for server-rendered HTML applications, especially when combined with htmx.
Basic Setup
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from jx import Catalog
app = FastAPI()
catalog = Catalog("components/", auto_reload=True)
@app.get("/", response_class=HTMLResponse)
def home():
return catalog.render("pages/home.jinja")
Passing Request Context
Use FastAPI's dependency injection to pass request data to components:
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from jx import Catalog
app = FastAPI()
catalog = Catalog("components/", auto_reload=True)
@app.get("/", response_class=HTMLResponse)
def home(request: Request):
return catalog.render(
"pages/home.jinja",
globals={"request": request},
)
Creating a Render Helper
Simplify rendering with a helper function:
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from jx import Catalog
app = FastAPI()
catalog = Catalog("components/", auto_reload=True)
def render(request: Request, component: str, status_code: int = 200, **context):
"""Render a Jx component with request context."""
html = catalog.render(
component,
globals={"request": request},
**context,
)
return HTMLResponse(html, status_code=status_code)
@app.get("/")
def home(request: Request):
return render(request, "pages/home.jinja")
@app.get("/products")
def products(request: Request):
products = get_products()
return render(request, "pages/products.jinja", products=products)
Using Dependency Injection
For cleaner code, create a dependency that provides the render function:
from fastapi import Request
from fastapi.responses import HTMLResponse
from jx import Catalog
catalog = Catalog("components/", auto_reload=True)
class Templates:
def __init__(self, request: Request):
self.request = request
def render(self, component: str, status_code: int = 200, **context):
html = catalog.render(
component,
globals={"request": self.request},
**context,
)
return HTMLResponse(html, status_code=status_code)
from fastapi import FastAPI, Depends
from dependencies import Templates
app = FastAPI()
@app.get("/")
def home(templates: Templates = Depends()):
return templates.render("pages/home.jinja")
@app.get("/products/{product_id}")
def product_detail(product_id: int, templates: Templates = Depends()):
product = get_product(product_id)
return templates.render("pages/product.jinja", product=product)
URL Generation
FastAPI doesn't have a built-in url_for like Flask. Use request.url_for():
class Templates:
def __init__(self, request: Request):
self.request = request
def render(self, component: str, status_code: int = 200, **context):
html = catalog.render(
component,
globals={
"request": self.request,
"url_for": self.request.url_for,
},
**context,
)
return HTMLResponse(html, status_code=status_code)
<nav>
<a href="{{ url_for('home') }}">Home</a>
<a href="{{ url_for('products') }}">Products</a>
<a href="{{ url_for('about') }}">About</a>
</nav>
Make sure your routes have names:
@app.get("/", name="home")
def home(templates: Templates = Depends()):
return templates.render("pages/home.jinja")
@app.get("/products", name="products")
def products(templates: Templates = Depends()):
return templates.render("pages/products.jinja")
Static Files
Mount a static files directory and create a helper:
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
class Templates:
def __init__(self, request: Request):
self.request = request
def render(self, component: str, status_code: int = 200, **context):
def static(path: str) -> str:
return self.request.url_for("static", path=path)
html = catalog.render(
component,
globals={
"request": self.request,
"url_for": self.request.url_for,
"static": static,
},
**context,
)
return HTMLResponse(html, status_code=status_code)
{#def title #}
<!DOCTYPE html>
<html>
<head>
<title>{{ title }}</title>
<link rel="icon" href="{{ static('favicon.ico') }}">
{% for css in assets.collect_css() %}
<link rel="stylesheet" href="{{ static(css) }}">
{% endfor %}
</head>
<body>
{{ content }}
{% for js in assets.collect_js() %}
<script type="module" src="{{ static(js) }}"></script>
{% endfor %}
</body>
</html>
Flash Messages
FastAPI doesn't have built-in flash messages. Here's a simple session-based implementation:
from starlette.middleware.sessions import SessionMiddleware
from fastapi import Request
def flash(request: Request, message: str, category: str = "info"):
"""Add a flash message to the session."""
if "_flashes" not in request.session:
request.session["_flashes"] = []
request.session["_flashes"].append({"message": message, "category": category})
def get_flashed_messages(request: Request):
"""Get and clear flash messages from the session."""
messages = request.session.pop("_flashes", [])
return messages
from fastapi import FastAPI
from starlette.middleware.sessions import SessionMiddleware
app = FastAPI()
app.add_middleware(SessionMiddleware, secret_key="your-secret-key")
from flash import get_flashed_messages
class Templates:
def __init__(self, request: Request):
self.request = request
def render(self, component: str, status_code: int = 200, **context):
html = catalog.render(
component,
globals={
"request": self.request,
"url_for": self.request.url_for,
"messages": get_flashed_messages(self.request),
},
**context,
)
return HTMLResponse(html, status_code=status_code)
{#css messages.css #}
{% if messages %}
<div class="messages">
{% for msg in messages %}
<div class="message message-{{ msg.category }}">
{{ msg.message }}
</div>
{% endfor %}
</div>
{% endif %}
from fastapi import Depends
from fastapi.responses import RedirectResponse
from flash import flash
@app.post("/items")
def create_item(request: Request, templates: Templates = Depends()):
# ... create item ...
flash(request, "Item created successfully!", "success")
return RedirectResponse(url="/items", status_code=303)
Authentication
Integrate with your authentication system:
from fastapi import Request, Depends
from auth import get_current_user # Your auth implementation
class Templates:
def __init__(self, request: Request, user = Depends(get_current_user)):
self.request = request
self.user = user
def render(self, component: str, status_code: int = 200, **context):
html = catalog.render(
component,
globals={
"request": self.request,
"url_for": self.request.url_for,
"user": self.user,
},
**context,
)
return HTMLResponse(html, status_code=status_code)
<nav>
<a href="{{ url_for('home') }}">Home</a>
{% if user %}
<span>{{ user.email }}</span>
<a href="{{ url_for('logout') }}">Logout</a>
{% else %}
<a href="{{ url_for('login') }}">Login</a>
{% endif %}
</nav>
Working with htmx
FastAPI and htmx are a great combination. Jx makes it easy to return HTML partials:
from fastapi import FastAPI, Request, Depends
from dependencies import Templates, catalog
app = FastAPI()
@app.get("/")
def home(templates: Templates = Depends()):
items = get_items()
return templates.render("pages/home.jinja", items=items)
@app.get("/items")
def item_list(request: Request):
"""Return just the item list partial for htmx requests."""
items = get_items()
return HTMLResponse(catalog.render("partials/item-list.jinja", items=items))
@app.post("/items")
def create_item(request: Request):
item = create_new_item()
# Return the new item partial to be swapped in
return HTMLResponse(catalog.render("partials/item.jinja", item=item))
@app.delete("/items/{item_id}")
def delete_item(item_id: int):
delete_item_by_id(item_id)
return HTMLResponse("") # Empty response removes the element
{#import "../layout.jinja" as Layout #}
{#import "../partials/item-list.jinja" as ItemList #}
{#def items #}
<Layout title="Items">
<h1>Items</h1>
<form hx-post="/items" hx-target="#item-list" hx-swap="beforeend">
<input type="text" name="name" placeholder="New item">
<button type="submit">Add</button>
</form>
<div id="item-list">
<ItemList items={{ items }} />
</div>
</Layout>
{#def item #}
<div id="item-{{ item.id }}" class="item">
<span>{{ item.name }}</span>
<button hx-delete="/items/{{ item.id }}" hx-target="#item-{{ item.id }}" hx-swap="outerHTML">
Delete
</button>
</div>
Lifespan Events
Use FastAPI's lifespan to preload components in production:
from contextlib import asynccontextmanager
from fastapi import FastAPI
from jx import Catalog
import os
is_production = os.getenv("ENV") == "production"
catalog = Catalog(auto_reload=not is_production)
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup: add folders with preloading in production
catalog.add_folder("components/", preload=is_production)
yield
# Shutdown: cleanup if needed
app = FastAPI(lifespan=lifespan)
Error Handling
Create custom error pages:
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.exceptions import HTTPException
from jx import Catalog
app = FastAPI()
catalog = Catalog("components/", auto_reload=True)
@app.exception_handler(404)
def not_found(request: Request, exc: HTTPException):
html = catalog.render(
"errors/404.jinja",
globals={"request": request},
)
return HTMLResponse(html, status_code=404)
@app.exception_handler(500)
def server_error(request: Request, exc: Exception):
html = catalog.render("errors/500.jinja")
return HTMLResponse(html, status_code=500)
Complete Example
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request, Depends
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from starlette.middleware.sessions import SessionMiddleware
from jx import Catalog
import os
# Configuration
is_production = os.getenv("ENV") == "production"
catalog = Catalog(auto_reload=not is_production)
# Flash messages
def flash(request: Request, message: str, category: str = "info"):
if "_flashes" not in request.session:
request.session["_flashes"] = []
request.session["_flashes"].append({"message": message, "category": category})
def get_flashed_messages(request: Request):
return request.session.pop("_flashes", [])
# Templates dependency
class Templates:
def __init__(self, request: Request):
self.request = request
def render(self, component: str, status_code: int = 200, **context):
html = catalog.render(
component,
globals={
"request": self.request,
"url_for": self.request.url_for,
"static": lambda path: self.request.url_for("static", path=path),
"messages": get_flashed_messages(self.request),
},
**context,
)
return HTMLResponse(html, status_code=status_code)
# Lifespan
@asynccontextmanager
async def lifespan(app: FastAPI):
catalog.add_folder("components/", preload=is_production)
yield
# App
app = FastAPI(lifespan=lifespan)
app.add_middleware(SessionMiddleware, secret_key="your-secret-key")
app.mount("/static", StaticFiles(directory="static"), name="static")
# Routes
@app.get("/", name="home")
def home(templates: Templates = Depends()):
return templates.render("pages/home.jinja")
@app.get("/products", name="products")
def products(templates: Templates = Depends()):
products = get_products()
return templates.render("pages/products.jinja", products=products)
@app.get("/products/{product_id}", name="product_detail")
def product_detail(product_id: int, templates: Templates = Depends()):
product = get_product(product_id)
if not product:
raise HTTPException(status_code=404)
return templates.render("pages/product.jinja", product=product)
@app.post("/products", name="create_product")
def create_product(request: Request, templates: Templates = Depends()):
# ... create product ...
flash(request, "Product created!", "success")
return RedirectResponse(url="/products", status_code=303)
# Error handlers
@app.exception_handler(404)
def not_found(request: Request, exc):
html = catalog.render("errors/404.jinja", globals={"request": request})
return HTMLResponse(html, status_code=404)
{#import "./nav.jinja" as Nav #}
{#import "./messages.jinja" as Messages #}
{#css layout.css #}
{#def title #}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ title }}</title>
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
{% for css in assets.collect_css() %}
<link rel="stylesheet" href="{{ static(css) }}">
{% endfor %}
</head>
<body>
<Nav />
<Messages />
<main>
{{ content }}
</main>
{% for js in assets.collect_js() %}
<script type="module" src="{{ static(js) }}"></script>
{% endfor %}
</body>
</html>
Async Routes
Jx rendering is synchronous, but this is fine in async routes. Template rendering is CPU-bound and fast:
@app.get("/dashboard")
async def dashboard(templates: Templates = Depends()):
# Async database call
stats = await get_dashboard_stats()
# Sync template rendering (fast, CPU-bound)
return templates.render("pages/dashboard.jinja", stats=stats)
If you prefer, define sync routes and FastAPI will run them in a thread pool:
@app.get("/dashboard")
def dashboard(templates: Templates = Depends()):
stats = get_dashboard_stats() # sync
return templates.render("pages/dashboard.jinja", stats=stats)
Both approaches work well. Choose based on whether your data fetching is async or sync.