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
templates
object pointing to our templates directory router = APIRouter()
gives us a clean way to organize our routes- The
get_blogs
function usesHTMLResponse
instead of the default JSON response templates.TemplateResponse
renders 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.