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.
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.
Big thanks to Aditya Kabra for inviting me to speak, and for organizing and hosting this workshop.
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:
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
:
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]
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:
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]
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:
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]
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]
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]
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]
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]
@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]
# 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.