Claude Code for FastHTML: Python-First Web Apps Without JavaScript Frameworks — Claude Skills 360 Blog
Blog / Backend / Claude Code for FastHTML: Python-First Web Apps Without JavaScript Frameworks
Backend

Claude Code for FastHTML: Python-First Web Apps Without JavaScript Frameworks

Published: January 19, 2027
Read time: 8 min read
By: Claude Skills 360

FastHTML builds web applications entirely in Python: HTML elements are Python function calls, routing is Python decorators, and interactivity is HTMX attribute additions — no JavaScript framework, no build step, no transpilation. Div, Button, Form, Input are functions returning HTML strings. Routes return FT (FastTag) elements directly. hx-post, hx-target, and hx-swap drive server-side updates over HTTP. Datastar extends HTMX’s reactivity model. Claude Code generates FastHTML route handlers, component functions, form submissions with HTMX, and the server-sent event patterns for real-time Python web applications.

CLAUDE.md for FastHTML Projects

## FastHTML Stack
- Version: python-fasthtml >= 0.12
- UI components: FT elements (Div, P, Button, Form, Input, etc.) — all Python
- Interactivity: HTMX attributes (hx-get, hx-post, hx-target, hx-swap)
- Styling: PicoCSS (built-in) or Tailwind CDN — no build step
- DB: SQLite via sqlite-utils or fastlite for zero-config persistence
- Forms: Handle as dicts in route handlers, validate with Pydantic if needed
- Sessions: Starlette sessions via middleware for auth state
- SSE: server-sent events for live updates with EventStream

Basic App Structure

# main.py — FastHTML app with routes and components
from fasthtml.common import *
from datetime import datetime

app, rt = fast_app(
    hdrs=(
        Link(rel="stylesheet", href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"),
    ),
    live=True,   # Enable live reload in development
)

# In-memory store (replace with DB)
orders = {}


# Component: Python function returning FT elements
def order_card(order: dict) -> FT:
    status_color = {
        "pending": "secondary",
        "processing": "primary",
        "shipped": "contrast",
        "delivered": "success",
        "cancelled": "danger",
    }.get(order["status"], "secondary")

    return Article(
        Header(
            Span(f"#{order['id'][-6:]}", cls="mono"),
            Span(f"${order['amount'] / 100:.2f}", style="float:right; font-weight:bold"),
        ),
        P(f"Customer: {order['customer_id']}"),
        Footer(
            Mark(order["status"], cls=f"pico-background-{status_color}"),
            # HTMX: cancel button sends POST without full page reload
            *(
                [Button(
                    "Cancel",
                    hx_post=f"/orders/{order['id']}/cancel",
                    hx_target=f"#order-{order['id']}",
                    hx_swap="outerHTML",
                    hx_confirm="Cancel this order?",
                    cls="secondary small",
                )]
                if order["status"] == "pending" else []
            ),
        ),
        id=f"order-{order['id']}",
    )


def page_layout(*content, title="Orders") -> FT:
    return Html(
        Head(
            Title(title),
            Script(src="https://unpkg.com/[email protected]"),
            *app.hdrs,
        ),
        Body(
            Main(
                H1(title),
                *content,
                cls="container",
            )
        ),
    )


# Routes with FT return values
@rt("/")
def get():
    order_list = [order_card(o) for o in orders.values()]

    return page_layout(
        Div(
            H2("Your Orders"),
            *order_list if order_list else [P("No orders yet.", cls="secondary")],
            id="order-list",
        ),
        A("+ New Order", href="/orders/new", role="button"),
    )


@rt("/orders/new")
def get():
    return page_layout(
        H2("New Order"),
        Form(
            Label("Customer ID",
                Input(type="text", name="customer_id", placeholder="cust-123", required=True)
            ),
            Label("Amount (dollars)",
                Input(type="number", name="amount", min="1", step="0.01", required=True)
            ),
            Button("Place Order", type="submit"),
            method="post",
            action="/orders",
        ),
    )


@rt("/orders")
def post(customer_id: str, amount: float):
    """Handle order creation form submission."""
    import uuid

    order_id = str(uuid.uuid4())
    orders[order_id] = {
        "id": order_id,
        "customer_id": customer_id,
        "amount": int(amount * 100),  # Store as cents
        "status": "pending",
        "created_at": datetime.utcnow().isoformat(),
    }

    return RedirectResponse("/", status_code=303)


@rt("/orders/{order_id}/cancel")
def post(order_id: str):
    """HTMX partial update: cancel order, return updated card."""
    if order_id not in orders:
        return Div("Order not found", cls="error", id=f"order-{order_id}")

    orders[order_id]["status"] = "cancelled"

    # Return just the updated card — HTMX swaps it in
    return order_card(orders[order_id])

Database with fastlite

# db.py — SQLite persistence with fastlite
from fasthtml.common import *
from fastlite import database
from datetime import datetime

db = database("orders.db")

# Define table schema  
orders = db.t.orders
if not orders.exists():
    orders.create({
        "id": str,
        "customer_id": str,
        "amount": int,
        "status": str,
        "created_at": str,
    }, pk="id")

Order = orders.dataclass()


@rt("/orders")
def get():
    """List orders from SQLite."""
    all_orders = orders(order_by="created_at desc", limit=50)

    return page_layout(
        Div(
            *(order_card(o.__dict__) for o in all_orders),
            id="order-list",
        ),
    )


@rt("/orders")
def post(customer_id: str, amount: float):
    import uuid
    order = Order(
        id=str(uuid.uuid4()),
        customer_id=customer_id,
        amount=int(amount * 100),
        status="pending",
        created_at=datetime.utcnow().isoformat(),
    )
    orders.insert(order)
    return RedirectResponse("/", status_code=303)
# Search with HTMX infinite scroll and debounce
@rt("/orders/search")
def get(q: str = "", page: int = 0):
    """HTMX partial: returns matching order cards."""
    page_size = 20

    results = orders(
        where="customer_id LIKE ? OR id LIKE ?",
        where_args=[f"%{q}%", f"%{q}%"],
        limit=page_size + 1,
        offset=page * page_size,
    )

    has_more = len(results) > page_size
    display = results[:page_size]

    cards = [order_card(o.__dict__) for o in display]

    if has_more:
        cards.append(
            Div(
                # Infinite scroll trigger: loads next page when visible
                hx_get=f"/orders/search?q={q}&page={page + 1}",
                hx_trigger="revealed",
                hx_swap="outerHTML",
                cls="loading-sentinel",
            )
        )

    return Div(*cards, id=f"search-page-{page}")


# Route for search input with debounce
def search_bar() -> FT:
    return Input(
        type="search",
        name="q",
        placeholder="Search orders...",
        # hx-get triggers search on keystroke (debounced)
        hx_get="/orders/search",
        hx_target="#search-results",
        hx_trigger="keyup changed delay:300ms",
        hx_swap="innerHTML",
        hx_push_url="false",
    )

Server-Sent Events for Real-Time

# SSE: push updates to connected clients
from starlette.responses import EventSourceResponse
import asyncio


@rt("/events/orders")
async def get(request):
    """Stream order updates via SSE."""

    async def event_generator():
        last_count = 0

        while True:
            current_count = orders.count

            if current_count != last_count:
                # New order: send updated count badge
                badge_html = str(Span(str(current_count), id="order-count", cls="badge"))
                yield {"data": badge_html, "event": "order-update"}
                last_count = current_count

            await asyncio.sleep(2)

            # Check if client disconnected
            if await request.is_disconnected():
                break

    return EventSourceResponse(event_generator())


# Client-side: listen for SSE with HTMX
def order_count_badge() -> FT:
    return Div(
        Span(str(orders.count), id="order-count", cls="badge"),
        # HTMX SSE extension: swap content on server events
        hx_ext="sse",
        sse_connect="/events/orders",
        sse_swap="order-update",
        hx_target="#order-count",
        hx_swap="outerHTML",
    )


serve()   # Start the app

For the Django full-stack framework with ORM, admin, and batteries-included Python web development without FastHTML’s simplicity tradeoffs, see the patterns in the Django REST framework guide. For the htmx-alpine.js pattern that uses HTMX from a traditional HTML template system with Alpine for lightweight client state, the HTMX guide covers Alpine.js integration. The Claude Skills 360 bundle includes FastHTML skill sets covering component patterns, HTMX interactivity, and database integration. Start with the free tier to try FastHTML app generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free