Skip to content

FastAPI Example

A small blog application showing how Fragments fits into a real FastAPI project. It covers layout components, component composition, conditional rendering, and list rendering across two routes.

Project structure

app/
├── main.py        # FastAPI app and loader registration
├── models.py      # Data model
├── components.py  # Reusable components
└── routes.py      # Route handlers

models.py

A plain dataclass to represent a blog post:

from dataclasses import dataclass, field

@dataclass
class Post:
    slug: str
    title: str
    summary: str
    body: str
    author: str
    published: bool = True

components.py

Two components: a full-page layout wrapper, and a post card used in the listing.

from fragments.types import Children
from models import Post

def Layout(children: Children, title: str = "My Blog") -> str:
    return <>
        <html lang="en">
            <head>
                <meta charset="utf-8" />
                <title>{{ title }}</title>
                <meta name="viewport" content="width=device-width, initial-scale=1" />
                <link rel="stylesheet" href="/static/styles.css" />
            </head>
            <body>
                <header>
                    <a href="/" classes="site-title">My Blog</a>
                </header>
                <main>
                    <Children... />
                </main>
            </body>
        </html>
    </>


def PostCard(post: Post) -> str:
    return <>
        <article classes="post-card">
            <h2>
                <a href={{ f"/posts/{post.slug}" }}>{{ post.title }}</a>
            </h2>
            <p classes="summary">{{ post.summary }}</p>
            <p classes="byline">By {{ post.author }}</p>
        </article>
    </>

Layout receives children from whatever is nested inside <Layout>...</Layout> and title as a keyword argument. PostCard is always used self-closing, so it declares only post — no children parameter needed.

routes.py

Two routes: a post listing and a detail view.

from fastapi import APIRouter, HTTPException
from fastapi.responses import HTMLResponse
from models import Post
from components import Layout, PostCard

router = APIRouter()

POSTS = [
    Post(
        slug="getting-started",
        title="Getting Started with Fragments",
        summary="How to add Python Fragments to an existing FastAPI app.",
        body="<p>Python Fragments lets you write HTML directly in .py files...</p>",
        author="Alice",
    ),
    Post(
        slug="component-patterns",
        title="Component Patterns",
        summary="Layout wrappers, cards, and other patterns that keep views clean.",
        body="<p>The key insight is that a component is just a function...</p>",
        author="Bob",
    ),
    Post(
        slug="upcoming-features",
        title="Upcoming Features",
        summary="What's coming next.",
        body="",
        author="Alice",
        published=False,
    ),
]

_by_slug: dict[str, Post] = {p.slug: p for p in POSTS}


@router.get("/", response_class=HTMLResponse)
async def index() -> str:
    published = [p for p in POSTS if p.published]
    return <>
        <Layout title="My Blog">
            <h1>Latest Posts</h1>
            <PostCard for={{ post in published }} post={{ post }} />
        </Layout>
    </>


@router.get("/posts/{slug}", response_class=HTMLResponse)
async def post_detail(slug: str) -> str:
    post = _by_slug.get(slug)
    if post is None or not post.published:
        raise HTTPException(status_code=404)
    return <>
        <Layout title={{ post.title }}>
            <article>
                <h1>{{ post.title }}</h1>
                <p classes="byline"><em>By {{ post.author }}</em></p>
                {{ post.body }}
            </article>
            <a href="/" classes="back-link"> All posts</a>
        </Layout>
    </>

The for attribute on <PostCard> renders one card per post, passing each post object through as an attribute. Unpublished posts are filtered in Python before the fragment runs — keeping the markup free of business logic.

main.py

Register the loader before any fragment-containing modules are imported:

from fragments import loader  # isort: skip

from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from routes import router

app = FastAPI()
app.include_router(router)

The # isort: skip comment prevents import sorters from moving the loader import below routes, which would cause fragment syntax to be parsed before the transpiler is active.