Time to build something exciting! We're creating a blog system using FastAPI with proper template rendering. Let's dive into the structure and see how everything connects.
🌳 C:\Users\soura\Documents\mine\fastapi\blog
├── 📁 alembic
|  ├── 📄 env.py
├── 📄 alembic.ini
├── 📁 apis
├── 📁 apps   #new
|  ├── 📄 main.py
|  └── 📁 v1
|     └── 📄 blog.py 
├── 📁 core
|  ├── 📄 config.py
|  └── 📄 security.py
├── 📁 database
|  ├── 📁 crud
|  |  ├── 📄 blog.py
|  |  └── 📄 user.py
|  ├── 📄 db.py
|  └── 📁 models
|     ├── 📄 base.py
|     ├── 📄 blog.py
|     ├── 📄 user.py
|     └── 📄 __init__.py
├── 📄 docker-compose.yaml
├── 📄 Dockerfile
├── 📄 main.py
├── 📄 requirements.txt
├── 📁 schemas
|  ├── 📄 blog.py
|  ├── 📄 user.py
|  └── 📁 __pycache__
|     └── 📄 user.cpython-311.pyc
└── 📁 templates  #new
   └── 📁 blog
      └── 📄 list.html
Setting Up Dependencies
First things first, let's get our requirements sorted. Create a requirements.txt file and add jinja in the dependency:
fastapi==0.115.12
uvicorn==0.27.1
python-dotenv==1.1.1
psycopg2-binary==2.9.10
sqlmodel==0.0.24
alembic==1.16.4
PyJWT==2.10.1
python-multipart==0.0.20
#new
Jinja2==3.1.6
The new addition here is Jinja2==3.1.6 - our templating engine that'll help us serve beautiful HTML pages instead of just JSON responses.
Let's start with the blog functionality. In apps/v1/blog.py:
from fastapi import APIRouter
from fastapi import Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
templates = Jinja2Templates(directory="templates")
router = APIRouter()
@router.get("/", response_class=HTMLResponse)
def get_blogs(request: Request):
    return templates.TemplateResponse("blog/list.html", {"request": request})
Notice we're using response_class=HTMLResponse. This tells FastAPI we're returning HTML, not JSON. The async keyword isn't strictly necessary for this simple function, but it's good practice.
Breaking this down:
- We create a 
templatesobject pointing to our templates directory router = APIRouter()gives us a clean way to organize our routes- The 
get_blogsfunction usesHTMLResponseinstead of the default JSON response templates.TemplateResponserenders our HTML template with the request context
The key insight here? Always pass the request object in the context dictionary. Templates need access to request data for things like URL generation, user information, and more.
App-Level Routing Structure
Our routing follows a clean, organized pattern. Create a apps/main.py file:
from fastapi import APIRouter
from apps.v1.blog import router as blog_router
app_router = APIRouter()
app_router.include_router(blog_router, prefix="/blogs", tags=["blogs app"])
The main.py at root ties everything together.
from fastapi import FastAPI
#...
from apis.main import api_router
from apps.main import app_router
app: FastAPI = FastAPI(title=settings.TITLE, description=settings.DESCRIPTION, version=settings.VERSION)
app.include_router(api_router)
app.include_router(app_router)
HTML Template with Modern Styling
Let's create templates/blog/list.html. Our blog list template uses Tailwind CSS via CDN.
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
    <title>Blog List</title>
</head>
<body>
    <h1 class="text-3xl font-bold text-center">Blog List</h1>
</body>
</html>
Using Tailwind's browser version is perfect for rapid prototyping. The text-3xl font-bold text-center classes give us a clean, centered heading without writing custom CSS.