Back to Blog

FastAPI HTML Templates: From Basics to Production

by Peter Szalontay, November 05, 2024

FastAPI HTML Templates: From Basics to Production

Automate Your Business with AI

Enterprise-grade AI agents customized for your needs

Discover Lazy AI for Business

You may be aware of FastAPI’s in terms of API development. But people fail to realize that is transforming how HTML templates work in full-stack development. In this guide, we’re going to explore my three years of experience with this tool and cover everything you should know about using FastAPI HTML templates.

Personal Experience Note: Okay, I have to admit that I was skeptical at first. However, since I’ve started, I’ve built several production applications, including a CMS that serves over 100,000 daily users. And what I've found is that FastAPI's template support is not just adequate – it's exceptional when you use it right.

Getting Started: The Complete Environment Setup

Before we jump into the topic of templates, let's set up a good development environment. Below, I’m sharing the configuration that’s most reliable for my projects:

# Create a virtual environment
python -m venv venv

# Activate it
source venv/bin/activate  # On Windows: venv\Scripts\activate

# Install dependencies
pip install fastapi[all] uvicorn jinja2 python-multipart aiofiles python-dotenv

Real-World Project Structure

I’ve gone through trial and error. The result? I've developed this project structure that you can use to scale as your application grows:

my_project/
├── app/
│   ├── __init__.py
│   ├── main.py
│   ├── config.py
│   ├── routes/
│   │   ├── __init__.py
│   │   ├── home.py
│   │   └── admin.py
│   ├── templates/
│   │   ├── base.html
│   │   ├── components/
│   │   │   ├── navbar.html
│   │   │   └── footer.html
│   │   └── pages/
│   │       ├── home.html
│   │       └── admin.html
│   └── static/
│       ├── css/
│       ├── js/
│       └── images/
└── requirements.txt

Implementing - A Step-by-Step Guide

Step 1: How to Set Up the Configuration

First, we’ll make a config.py file to manage our application settings:

# app/config.py
from pydantic_settings import BaseSettings
from functools import lru_cache

class Settings(BaseSettings):
    APP_NAME: str = "MyFastAPIApp"
    DEBUG: bool = False
    TEMPLATE_DIR: str = "templates"
    STATIC_DIR: str = "static"
    
    class Config:
        env_file = ".env"

@lru_cache()
def get_settings():
    return Settings()

Step 2: Creating the Main Application

Here's how to set up your main.py with proper template configuration:

# app/main.py
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles
from pathlib import Path
from .config import get_settings

settings = get_settings()
app = FastAPI(title=settings.APP_NAME)

# Get the absolute path to the static and template directories
BASE_DIR = Path(__file__).resolve().parent
static_dir = BASE_DIR / settings.STATIC_DIR
template_dir = BASE_DIR / settings.TEMPLATE_DIR

# Mount static files
app.mount("/static", StaticFiles(directory=static_dir), name="static")

# Initialize templates with custom filters and globals
templates = Jinja2Templates(directory=template_dir)
templates.env.globals.update({
    "app_name": settings.APP_NAME,
    "debug": settings.DEBUG
})

Step 3: Creating Base Template

A production-ready base template that I've refined over multiple projects:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>{% block title %}{{ app_name }}{% endblock %}</title>
    
    <!-- Favicon -->
    <link rel="icon" type="image/x-icon" href="{{ url_for('static', path='images/favicon.ico') }}">
    
    <!-- CSS -->
    <link rel="stylesheet" href="{{ url_for('static', path='css/main.css') }}">
    {% block extra_css %}{% endblock %}
    
    <!-- Security Headers -->
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src 'self' data: https:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';">
    <meta name="referrer" content="strict-origin-when-cross-origin">
    
    <!-- Open Graph Meta Tags -->
    {% block meta %}
    <meta property="og:title" content="{{ app_name }}">
    <meta property="og:description" content="{% block meta_description %}{% endblock %}">
    <meta property="og:image" content="{% block meta_image %}{% endblock %}">
    {% endblock %}
</head>
<body class="{% block body_class %}{% endblock %}">
    <!-- Header -->
    <header>
        {% include "components/navbar.html" %}
        {% block header %}{% endblock %}
    </header>

    <!-- Flash Messages -->
    {% if messages %}
    <div class="messages">
        {% for message in messages %}
            <div class="alert alert-{{ message.type }}">
                {{ message.text }}
                <button type="button" class="close" data-dismiss="alert">&times;</button>
            </div>
        {% endfor %}
    </div>
    {% endif %}

    <!-- Main Content -->
    <main>
        {% block content %}{% endblock %}
    </main>

    <!-- Footer -->
    <footer>
        {% include "components/footer.html" %}
        {% block footer %}{% endblock %}
    </footer>

    <!-- JavaScript -->
    <script src="{{ url_for('static', path='js/main.js') }}" defer></script>
    {% if debug %}
    <script src="{{ url_for('static', path='js/debug.js') }}" defer></script>
    {% endif %}
    {% block extra_js %}{% endblock %}

    <!-- Error Handling -->
    {% if error %}
    <script>
        console.error('{{ error|tojson }}');
    </script>
    {% endif %}

    <!-- CSRF Protection -->
    {% if csrf_token %}
    <script>
        const csrfToken = '{{ csrf_token }}';
        document.addEventListener('DOMContentLoaded', () => {
            document.querySelectorAll('form').forEach(form => {
                const input = document.createElement('input');
                input.type = 'hidden';
                input.name = 'csrf_token';
                input.value = csrfToken;
                form.appendChild(input);
            });
        });
    </script>
    {% endif %}
</body>
</html>

Step 4: Implementing Route Handlers

Let's create a route handler that demonstrates real-world usage:

# app/routes/home.py
from fastapi import APIRouter, Request, Form, HTTPException
from fastapi.templating import Jinja2Templates
from typing import Optional

router = APIRouter()
templates = Jinja2Templates(directory="templates")

@router.get("/")
async def home(request: Request, message: Optional[str] = None):
    context = {
        "request": request,
        "message": message,
        "featured_items": [
            {"title": "Item 1", "description": "Description 1"},
            {"title": "Item 2", "description": "Description 2"},
        ]
    }
    return templates.TemplateResponse("pages/home.html", context)

Production Tip: In my experience, one of the most common mistakes is not properly handling template context. You should always make sure to include the 'request' object in the context. Furthermore, you should use try-except blocks for database operations or external service calls.

Frequently Asked Questions (FAQ)

Q: Is FastAPI template performance really that different from traditional frameworks like Django?

The short answer? Yes. FastAPI with Jinja2 templates can do about 2-3x more requests per second, if we’re comparing to Django. But aside from that, FastAPI can handle async operations while rendering templates.

Q: Can I use modern frontend frameworks like React with these templates?

Yes, you absolutely can! I've successfully used hybrid approaches myself. I’ll use simple Jinja2 templates on some pages and React on others. You want to make specific endpoints for your SPA sections and use templates for server-rendered pages.

Q: What changes with user authentication when you’re using FastAPI templates?

FastAPI has a dependency injection system. It works seamlessly with templates. Here's a pattern that I like to use:

from fastapi import Depends, HTTPException
from fastapi.security import HTTPBearer

security = HTTPBearer()

async def get_current_user(request: Request):
    token = request.cookies.get("auth_token")
    if not token:
        raise HTTPException(status_code=401)
    # Verify token and return user
    return user

@app.get("/dashboard")
async def dashboard(
    request: Request,
    current_user = Depends(get_current_user)
):
    return templates.TemplateResponse(
        "dashboard.html",
        {"request": request, "user": current_user}
    )

Performance Optimization Tips

Once I have optimized a few high-traffic FastAPI applications, I recommend this for better performance:

# Enable template caching in production
if not settings.DEBUG:
    templates.env.auto_reload = False
    templates.env.cache_size = 1000

# Use template partials for frequently updated content
@app.get("/partial/notifications")
async def notifications_partial(request: Request):
    notifications = await get_user_notifications(request)
    return templates.TemplateResponse(
        "partials/notifications.html",
        {"request": request, "notifications": notifications}
    )

Conclusion

Hopefully, this guide has illustrated how beneficial FastAPI HTML templates can be. I’ve been working with them for years now. Trust me when I say that they’re an excellent solution for making modern web applications. FastAPI brings the speed, and Jinja2 brings the flexibility. Together, you get a really powerful platform. And, I think you’ll find that both your simple websites and complex web applications can benefit from it.

Remember: Don’t neglect templates in FastAPI! They're high-quality and necessary for any developer. They can help you scale and easily maintain your web applications over time. You just have to get familiar with the structure, common patterns, and ways to optimize performance.

Please, don’t hesitate to reach out! You can use the comments section below or check the official FastAPI documentation page if you have any further questions..

Automate Your Business with AI

Enterprise-grade AI agents customized for your needs

Discover Lazy AI for Business

Recent blog posts