Jinja Templates with FastAPI and Tailwind CSS

While FastAPI excels at building APIs, it also supports server-side rendering with Jinja2 templates. Combined with Tailwind CSS via CDN, you can quickly build beautiful, responsive web pages without a complex frontend build process.

Project Setup

Install Dependencies

pip install fastapi uvicorn jinja2 python-multipart

Project Structure

fastapi-jinja/
├── app/
│ ├── __init__.py
│ ├── main.py
│ └── templates/
│ ├── base.html
│ ├── index.html
│ ├── about.html
│ └── components/
│ ├── navbar.html
│ ├── footer.html
│ └── card.html
└── static/
└── css/
└── custom.css

Basic Setup

Configure Jinja2 Templates

# app/main.py
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse

app = FastAPI()

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

# Setup Jinja2 templates
templates = Jinja2Templates(directory="app/templates")

@app.get("/", response_class=HTMLResponse)
async def home(request: Request):
return templates.TemplateResponse(
"index.html",
{"request": request, "title": "Home"}
)

@app.get("/about", response_class=HTMLResponse)
async def about(request: Request):
return templates.TemplateResponse(
"about.html",
{"request": request, "title": "About"}
)

Base Template with Tailwind CSS

Create a base template that includes Tailwind CSS via CDN:

<!-- app/templates/base.html -->
<!DOCTYPE html>
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ title }}{% endblock %} | My App</title>

<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>

<!-- Custom Tailwind Config -->
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#0d9488',
secondary: '#64748b',
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
}
}
}
}
</script>

<!-- Custom styles -->
<style type="text/tailwindcss">
@layer components {
.btn-primary {
@apply px-4 py-2 bg-primary text-white rounded-lg hover:bg-teal-700 transition-colors;
}
.btn-secondary {
@apply px-4 py-2 bg-secondary text-white rounded-lg hover:bg-slate-600 transition-colors;
}
.card {
@apply bg-white rounded-xl shadow-md overflow-hidden hover:shadow-lg transition-shadow;
}
}
</style>

{% block head %}{% endblock %}
</head>
<body class="h-full bg-slate-50 text-slate-900">
<div class="min-h-full flex flex-col">
<!-- Navbar -->
{% include "components/navbar.html" %}

<!-- Main Content -->
<main class="flex-1">
{% block content %}{% endblock %}
</main>

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

{% block scripts %}{% endblock %}
</body>
</html>

Reusable Components

<!-- app/templates/components/navbar.html -->
<nav class="bg-white shadow-sm sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<!-- Logo -->
<div class="flex items-center">
<a href="/" class="text-xl font-bold text-primary">
MyApp
</a>
</div>

<!-- Navigation Links -->
<div class="hidden md:flex items-center space-x-8">
<a href="/" class="text-slate-600 hover:text-primary transition-colors {% if request.url.path == '/' %}text-primary font-medium{% endif %}">
Home
</a>
<a href="/about" class="text-slate-600 hover:text-primary transition-colors {% if request.url.path == '/about' %}text-primary font-medium{% endif %}">
About
</a>
<a href="/contact" class="text-slate-600 hover:text-primary transition-colors {% if request.url.path == '/contact' %}text-primary font-medium{% endif %}">
Contact
</a>
</div>

<!-- CTA Button -->
<div class="flex items-center">
<a href="/login" class="btn-primary">
Get Started
</a>
</div>

<!-- Mobile Menu Button -->
<div class="md:hidden flex items-center">
<button id="mobile-menu-btn" class="text-slate-600 hover:text-primary">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
</svg>
</button>
</div>
</div>
</div>

<!-- Mobile Menu -->
<div id="mobile-menu" class="hidden md:hidden bg-white border-t">
<div class="px-4 py-3 space-y-2">
<a href="/" class="block py-2 text-slate-600 hover:text-primary">Home</a>
<a href="/about" class="block py-2 text-slate-600 hover:text-primary">About</a>
<a href="/contact" class="block py-2 text-slate-600 hover:text-primary">Contact</a>
</div>
</div>
</nav>

<script>
document.getElementById('mobile-menu-btn').addEventListener('click', () => {
document.getElementById('mobile-menu').classList.toggle('hidden');
});
</script>
<!-- app/templates/components/footer.html -->
<footer class="bg-slate-900 text-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
<!-- Brand -->
<div class="col-span-1 md:col-span-2">
<h3 class="text-xl font-bold text-primary mb-4">MyApp</h3>
<p class="text-slate-400 mb-4">
Building amazing web applications with FastAPI and Tailwind CSS.
</p>
</div>

<!-- Links -->
<div>
<h4 class="font-semibold mb-4">Quick Links</h4>
<ul class="space-y-2 text-slate-400">
<li><a href="/" class="hover:text-white transition-colors">Home</a></li>
<li><a href="/about" class="hover:text-white transition-colors">About</a></li>
<li><a href="/contact" class="hover:text-white transition-colors">Contact</a></li>
</ul>
</div>

<!-- Social -->
<div>
<h4 class="font-semibold mb-4">Follow Us</h4>
<div class="flex space-x-4">
<a href="#" class="text-slate-400 hover:text-white transition-colors">
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z"/>
</svg>
</a>
<a href="#" class="text-slate-400 hover:text-white transition-colors">
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
</a>
</div>
</div>
</div>

<div class="border-t border-slate-800 mt-8 pt-8 text-center text-slate-400">
<p>&copy; {{ current_year }} MyApp. All rights reserved.</p>
</div>
</div>
</footer>

Card Component

<!-- app/templates/components/card.html -->
{% macro card(title, description, image=None, link=None, tags=[]) %}
<div class="card">
{% if image %}
<div class="aspect-video overflow-hidden">
<img src="{{ image }}" alt="{{ title }}" class="w-full h-full object-cover hover:scale-105 transition-transform duration-300">
</div>
{% endif %}
<div class="p-6">
{% if tags %}
<div class="flex flex-wrap gap-2 mb-3">
{% for tag in tags %}
<span class="px-2 py-1 text-xs font-medium bg-primary/10 text-primary rounded-full">
{{ tag }}
</span>
{% endfor %}
</div>
{% endif %}
<h3 class="text-lg font-semibold text-slate-900 mb-2">{{ title }}</h3>
<p class="text-slate-600 mb-4">{{ description }}</p>
{% if link %}
<a href="{{ link }}" class="text-primary font-medium hover:underline">
Learn more &rarr;
</a>
{% endif %}
</div>
</div>
{% endmacro %}

Page Templates

Home Page

<!-- app/templates/index.html -->
{% extends "base.html" %}
{% from "components/card.html" import card %}

{% block content %}
<!-- Hero Section -->
<section class="bg-gradient-to-br from-primary to-teal-600 text-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24">
<div class="text-center">
<h1 class="text-4xl md:text-6xl font-bold mb-6">
Build Amazing Apps with FastAPI
</h1>
<p class="text-xl text-teal-100 mb-8 max-w-2xl mx-auto">
Modern, fast, and beautiful web applications using Python, Jinja2, and Tailwind CSS.
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="/docs" class="px-8 py-3 bg-white text-primary font-semibold rounded-lg hover:bg-teal-50 transition-colors">
Get Started
</a>
<a href="/github" class="px-8 py-3 border-2 border-white text-white font-semibold rounded-lg hover:bg-white/10 transition-colors">
View on GitHub
</a>
</div>
</div>
</div>
</section>

<!-- Features Section -->
<section class="py-20">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-16">
<h2 class="text-3xl font-bold text-slate-900 mb-4">Why Choose FastAPI?</h2>
<p class="text-slate-600 max-w-2xl mx-auto">
FastAPI combines the best of Python with modern web development practices.
</p>
</div>

<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
{% for feature in features %}
<div class="text-center p-6">
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
<svg class="w-8 h-8 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="{{ feature.icon }}"/>
</svg>
</div>
<h3 class="text-xl font-semibold text-slate-900 mb-2">{{ feature.title }}</h3>
<p class="text-slate-600">{{ feature.description }}</p>
</div>
{% endfor %}
</div>
</div>
</section>

<!-- Projects Section -->
<section class="py-20 bg-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-16">
<h2 class="text-3xl font-bold text-slate-900 mb-4">Featured Projects</h2>
</div>

<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
{% for project in projects %}
{{ card(
title=project.title,
description=project.description,
image=project.image,
link=project.link,
tags=project.tags
) }}
{% endfor %}
</div>
</div>
</section>

<!-- CTA Section -->
<section class="py-20 bg-slate-900 text-white">
<div class="max-w-4xl mx-auto px-4 text-center">
<h2 class="text-3xl font-bold mb-4">Ready to Get Started?</h2>
<p class="text-slate-400 mb-8">
Join thousands of developers building with FastAPI.
</p>
<a href="/signup" class="btn-primary text-lg px-8 py-3">
Start Building Today
</a>
</div>
</section>
{% endblock %}

About Page

<!-- app/templates/about.html -->
{% extends "base.html" %}

{% block content %}
<section class="py-20">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 class="text-4xl font-bold text-slate-900 mb-8">About Us</h1>

<div class="prose prose-lg max-w-none">
<p class="text-slate-600 mb-6">
We're passionate about building modern web applications with Python.
FastAPI combined with Jinja2 templates and Tailwind CSS provides
an excellent developer experience.
</p>

<h2 class="text-2xl font-bold text-slate-900 mt-12 mb-4">Our Stack</h2>

<div class="grid grid-cols-1 md:grid-cols-2 gap-6 not-prose">
{% for item in stack %}
<div class="flex items-start gap-4 p-4 bg-white rounded-lg shadow-sm">
<div class="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center flex-shrink-0">
<span class="text-2xl">{{ item.emoji }}</span>
</div>
<div>
<h3 class="font-semibold text-slate-900">{{ item.name }}</h3>
<p class="text-slate-600 text-sm">{{ item.description }}</p>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</section>
{% endblock %}

Updated Main Application

# app/main.py
from fastapi import FastAPI, Request, Form
from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse, RedirectResponse
from datetime import datetime

app = FastAPI()

app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="app/templates")

# Add global template variables
@app.middleware("http")
async def add_global_context(request: Request, call_next):
response = await call_next(request)
return response

# Custom template globals
templates.env.globals["current_year"] = datetime.now().year

# Sample data
FEATURES = [
{
"title": "Lightning Fast",
"description": "One of the fastest Python frameworks available, on par with NodeJS and Go.",
"icon": "M13 10V3L4 14h7v7l9-11h-7z"
},
{
"title": "Easy to Learn",
"description": "Intuitive design with excellent documentation and IDE support.",
"icon": "M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
},
{
"title": "Production Ready",
"description": "Built-in validation, serialization, and automatic API documentation.",
"icon": "M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
}
]

PROJECTS = [
{
"title": "E-Commerce Platform",
"description": "A full-featured online store with cart, checkout, and payment integration.",
"image": "https://images.unsplash.com/photo-1556742049-0cfed4f6a45d?w=800",
"link": "/projects/ecommerce",
"tags": ["FastAPI", "React", "Stripe"]
},
{
"title": "Task Management",
"description": "Collaborative task tracking with real-time updates and team features.",
"image": "https://images.unsplash.com/photo-1484480974693-6ca0a78fb36b?w=800",
"link": "/projects/tasks",
"tags": ["FastAPI", "WebSockets", "Vue"]
},
{
"title": "Analytics Dashboard",
"description": "Real-time data visualization and reporting for business intelligence.",
"image": "https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=800",
"link": "/projects/analytics",
"tags": ["FastAPI", "Charts", "PostgreSQL"]
}
]

STACK = [
{"name": "FastAPI", "emoji": "⚡", "description": "High-performance Python web framework"},
{"name": "Jinja2", "emoji": "📄", "description": "Powerful templating engine"},
{"name": "Tailwind CSS", "emoji": "🎨", "description": "Utility-first CSS framework"},
{"name": "PostgreSQL", "emoji": "🐘", "description": "Robust relational database"},
]

@app.get("/", response_class=HTMLResponse)
async def home(request: Request):
return templates.TemplateResponse(
"index.html",
{
"request": request,
"title": "Home",
"features": FEATURES,
"projects": PROJECTS
}
)

@app.get("/about", response_class=HTMLResponse)
async def about(request: Request):
return templates.TemplateResponse(
"about.html",
{
"request": request,
"title": "About",
"stack": STACK
}
)

# Form handling example
@app.get("/contact", response_class=HTMLResponse)
async def contact_form(request: Request):
return templates.TemplateResponse(
"contact.html",
{"request": request, "title": "Contact"}
)

@app.post("/contact")
async def contact_submit(
request: Request,
name: str = Form(...),
email: str = Form(...),
message: str = Form(...)
):
# Process the form (save to DB, send email, etc.)
print(f"Contact from {name} ({email}): {message}")

# Redirect with success message
return RedirectResponse(url="/contact?success=true", status_code=303)

Form Handling with Validation

<!-- app/templates/contact.html -->
{% extends "base.html" %}

{% block content %}
<section class="py-20">
<div class="max-w-2xl mx-auto px-4">
<h1 class="text-3xl font-bold text-slate-900 mb-8">Contact Us</h1>

{% if request.query_params.get('success') %}
<div class="mb-6 p-4 bg-green-50 border border-green-200 rounded-lg">
<p class="text-green-800">Thank you! Your message has been sent.</p>
</div>
{% endif %}

<form method="POST" class="space-y-6">
<div>
<label for="name" class="block text-sm font-medium text-slate-700 mb-2">
Name
</label>
<input
type="text"
id="name"
name="name"
required
class="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-shadow"
placeholder="Your name"
>
</div>

<div>
<label for="email" class="block text-sm font-medium text-slate-700 mb-2">
Email
</label>
<input
type="email"
id="email"
name="email"
required
class="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-shadow"
placeholder="you@example.com"
>
</div>

<div>
<label for="message" class="block text-sm font-medium text-slate-700 mb-2">
Message
</label>
<textarea
id="message"
name="message"
rows="5"
required
class="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-shadow resize-none"
placeholder="How can we help?"
></textarea>
</div>

<button type="submit" class="w-full btn-primary py-3 text-lg">
Send Message
</button>
</form>
</div>
</section>
{% endblock %}

Tips and Best Practices

1. Template Inheritance

Always use a base template for consistent layouts:

{% extends "base.html" %}
{% block content %}
<!-- Page-specific content -->
{% endblock %}

2. Reusable Macros

Create macros for repeated UI patterns:

{% macro button(text, variant="primary", href=None) %}
{% if href %}
<a href="{{ href }}" class="btn-{{ variant }}">{{ text }}</a>
{% else %}
<button class="btn-{{ variant }}">{{ text }}</button>
{% endif %}
{% endmacro %}

3. Flash Messages

Implement flash messages for user feedback:

from fastapi import Request
from starlette.middleware.sessions import SessionMiddleware

app.add_middleware(SessionMiddleware, secret_key="your-secret-key")

def flash(request: Request, message: str, category: str = "info"):
if "_messages" not in request.session:
request.session["_messages"] = []
request.session["_messages"].append({"message": message, "category": category})

def get_flashed_messages(request: Request):
messages = request.session.pop("_messages", [])
return messages

4. Static File Caching

Add cache busting for production:

import hashlib
import os

def get_file_hash(filepath: str) -> str:
with open(filepath, "rb") as f:
return hashlib.md5(f.read()).hexdigest()[:8]

templates.env.globals["static_url"] = lambda path: f"/static/{path}?v={get_file_hash(f'static/{path}')}"

Running the Application

# Development
uvicorn app.main:app --reload

# Production
gunicorn app.main:app -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000

Visit http://localhost:8000 to see your Tailwind-styled FastAPI app!


Conclusion

FastAPI with Jinja2 and Tailwind CSS CDN is a powerful combination for building server-rendered web applications. Benefits include:

  • Rapid prototyping - No build step required with Tailwind CDN
  • Full Python stack - Backend and templates in one language
  • Component reusability - Jinja macros and includes
  • Modern styling - Utility-first CSS with Tailwind

For production, consider using Tailwind CLI or a build process to purge unused CSS and optimize bundle size.

References


   Reprint policy


《Jinja Templates with FastAPI and Tailwind CSS》 by Isaac Zhou is licensed under a Creative Commons Attribution 4.0 International License
  TOC