FastTag Syntax & Multi-File Project Organization - Kentro Learn
Home / FastTag Syntax & Multi-File Project Organization

FastTag Syntax & Multi-File Project Organization

Understanding how to use and manipulate FastTags for HTML generation and organize large complext FastHTMN projects into multipple files.

By Isaac Flath • July 21, 2025
FastHTMLweb-dev

We'll dive into two key topics for mastering FastHTML. First, we'll explore best practices for using FastTag syntax, covering everything from simple conditionals and loops to creating reusable components. Then, we'll switch gears and look at four practical patterns for organizing your FastHTML project as it grows beyond a single file.

All the code examples for project organization are available in this GitHub repository.

Part 1: FastTag Syntax Patterns

FastHTML's templating is done with a "FastTag" syntax. The most important thing to understand is that you're not learning a new templating language; you're just using regular python approaches that work with any python library.

Understanding FastTag Syntax Basics

FastHTML tags map one-to-one with standard HTML elements. A Div() in Python becomes <div></div> in HTML. This direct translation means you can leverage your existing python and HTML knowledge. It's just a Pythonic syntax for the same structure. [01:30]

Here's a simple example:

python
from fasthtml.common import *

# This Python code...
my_ui = Div(P('Hello, FastHTML!'))

# ...renders this HTML:
# <div><p>Hello, FastHTML!</p></div>

Conditional Rendering with If Statements

You can use standard Python control flow, like if statements, to conditionally render content. This is perfect for situations like showing different content to logged-in vs. anonymous users. [02:20]

Let's say you have a variable is_logged_in:

python
is_logged_in = True
message = None

if is_logged_in:
    message = P('Welcome back!')
else:
    message = A('Please log in.', href='/login')

# 'message' will now contain the appropriate FastTag

Inline Conditionals and Ternary Operators

For simpler conditions, a full if/else block can be verbose. Python's ternary operator provides a more concise way to achieve the same result in a single line. This is great for simple text or component swaps. [03:12]

python
is_logged_in = False

message = P('Welcome back!') if is_logged_in else A('Please log in.', href='/login')

Conditional Styling Patterns

One of my most frequent uses for inline conditionals is dynamically changing styles. You can easily modify CSS classes or inline styles based on data, like changing the color of a score based on whether it's passing or failing. [03:40]

In this example, the text color changes based on the score variable:

python
score = 65

# The style attribute is set conditionally
status_display = P(
    f'Your score: {score}',
    style=f"color: {'green' if score >= 70 else 'red'}"
)

Creating Reusable Components

As you build your UI, you'll want to avoid repetition. In FastHTML, creating a "component" is as simple as defining a Python function that returns one or more FastTags. This is the fundamental pattern for building reusable UI elements. [05:25]

You can refactor any piece of UI logic into a function and then call it wherever you need it. [06:01]

python
def WelcomeMessage(is_logged_in: bool):
    if is_logged_in:
        return P('Welcome back!')
    else:
        return A('Please log in.', href='/login')

# Now you can use it like any other tag:
ui = Div(
    H1('My App'),
    WelcomeMessage(is_logged_in=True)
)

Content Inclusion/Exclusion Patterns

Sometimes you need to conditionally include or exclude entire blocks of content, like extra fields in a form. You can build up a list of FastTags and use if statements to append elements to it before rendering. [07:15]

Here's how you could conditionally add an email input to a form:

python
show_email = True
form_fields = [
    Input(type='text', name='name', placeholder='Name'),
]

if show_email:
    form_fields.append(
        Input(type='email', name='email', placeholder='Email')
    )

registration_form = Form(*form_fields)

Pattern Matching for Complex Conditionals

For more complex scenarios with multiple states, Python 3.10+'s match-case statement can be much cleaner than a long chain of if/elif/else. It's a great way to map a status variable to a specific UI representation. [08:50]

python
def StatusBadge(status: str):
    match status:
        case "active":
            return Span("Active", cl="badge-green")
        case "pending":
            return Span("Pending", cl="badge-yellow")
        case "inactive":
            return Span("Inactive", cl="badge-red")
        case _:
            return Span("Unknown", cl="badge-gray")

# Usage:
user_status_ui = StatusBadge(status="pending")

Iteration Patterns: Loops and Comprehensions

To render dynamic lists of items—like products, to-do items, or users—you'll need to iterate over your data.

For Loops

The classic for loop is a straightforward way to build a list of UI elements. You initialize an empty list and append a new component for each item in your data source. [11:00]

python
todos_data = ["Learn FastHTML", "Build an app", "Deploy to production"]
todo_items = []
for todo_text in todos_data:
    todo_items.append(Li(todo_text))

todo_list = Ul(*todo_items)

List Comprehensions

A list comprehension is a more concise, Pythonic way to do the same thing. For simple loops, I find it much more readable. [014:00]

python
todos_data = ["Learn FastHTML", "Build an app", "Deploy to production"]

todo_list = Ul(
    *[Li(todo_text) for todo_text in todos_data]
)

The map Function

Another powerful pattern, especially when you have a dedicated component function, is map(). It applies a function to every item in an iterable, which is perfect for rendering a list of components from a list of data. [14:30]

python
people_data = [
    {'name': 'Alice', 'age': 30},
    {'name': 'Bob', 'age': 25},
]

def PersonCard(person: dict):
    return Div(H3(person['name']), P(f"Age: {person['age']}"))

# The map function applies PersonCard to each item in people_data
people_list = Div(*map(PersonCard, people_data))

Live Example: HTMX Partials without Wrapper Divs

A common question is whether you need to wrap HTMX partial responses in a single Div. What if you want to return multiple sibling elements, like an H1 and a P tag, to be inserted into a container? [16:25]

The answer is you can return a tuple of FastTags from your route. FastHTML will render them as sibling elements, and HTMX will correctly place them in the target container. [23:02]

python
@app.get("/clicked")
async def get_clicked():
    # Returning a tuple of tags works!
    return H1('Clicked!'), P('Here is the new content.')

While this works, I often find it clearer to wrap the content in a single logical container, but it's good to know you don't have to.

Part 2: Multi-File Project Organization

When your app is small, a single file is perfectly fine. But as your project grows, you'll want to split it into multiple files for better organization and maintainability. We explore a Todo app that lets you organize todos by project, and edit, delete and mark them as complete.

When you start adding distinct areas of functionality (e.g., adding an email client to your to-do app), it's time to split things up. [27:00] I'll walk through four patterns using a sample To-Do application.

The Single-File Baseline

First, here's what the app looks like in a single file. The database models, routes, components, and application logic all live in main.py. This is our starting point. [25:00]

Pattern 1: Standard Python Modules

The simplest way to organize your project is with standard Python modules. You don't need a special framework feature; you just use Python's import system.

In this pattern, main.py still defines all the routes, but the logic is imported from other files like components.py, database.py, and pages.py. Your routes become very slim, acting as simple entry points that call out to your organized business and UI logic. [35:00]

Pattern 2: The Global App Pattern

This pattern uses a central globals.py file to define the main app instance and other shared resources like the database connection. Other files, like projects.py and todos.py, then import the app object from globals.py and attach their routes to it directly. Your main file just needs to import these modules to ensure the routes are registered. [37:55]

This can work well but requires discipline to prevent globals.py from becoming a dumping ground for unrelated code.

Pattern 3: The API Router Pattern

Inspired by FastAPI, FastHTML provides an APIRouter. This pattern decouples route definition from the application instance.

Each feature file (e.g., projects.py, todos.py) creates its own APIRouter instance and defines its routes on that router. The main main.py file then imports each router and includes it in the main application. [40:00]

API Router Benefits: URL Prefixes

A key advantage of the APIRouter is the ability to add a prefix to all routes within that router. This is incredibly useful for larger apps to avoid URL collisions and keep your routes logically grouped. [43:30]

For example, you can prefix all to-do related routes with /todos and all project routes with /projects. [44:00]

python
# in todos.py
ar = APIRouter(prefix='/todos')

@ar.get("/delete/{id}")  # Becomes /todos/delete/{id}
...

Bonus Topics

Mixing CSS Frameworks

If you need to mix CSS frameworks (e.g., Pico.css and Tailwind), be aware that their base styles and headers can conflict. [46:00] The best solution is to mount each part of your UI as a separate sub-app, which isolates their headers and prevents conflicts. However, the simplest approach is to stick with frameworks from the same ecosystem (e.g., multiple Tailwind-based component libraries) whenever possible.

Creating Reusable Component Libraries

Since FastHTML components are just Python functions, you can easily package them into a standard Python library and publish it to PyPI for reuse across projects. There's nothing special you need to do. This is exactly how libraries like Monster-UI work. [48:40]

Improving AI Assistant Context

When working with AI coding assistants like Cursor, you can significantly improve their performance by providing curated context. [51:30] Create context files (.cursor-rules) that contain simplified, relevant examples from the documentation that match your project's coding style. By removing irrelevant options and fixing dummy code, you guide the AI to generate code that is more consistent and correct for your specific use case.