路由与文章列表、详情页
本章你将学会用 APIRouter 拆分路由,实现首页文章列表和文章详情页。
APIRouter — FastAPI 的路由拆分方案
APIRouter 对标 Flask 的 Blueprint 和 Django 的 include()。
实例
# 文件路径:routers/posts.py
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.orm import Session
from database import get_db
from models import Post, Category
from schemas import PostResponse
router = APIRouter(
prefix="/posts", # 所有路由自动加上 /posts 前缀
tags=["文章"] # Swagger 文档中的分组标签
)
@router.get("/{post_id}", response_model=PostResponse, name="post_detail")
def post_detail(post_id: int, db: Session = Depends(get_db)):
"""文章详情(JSON API)"""
post = db.query(Post).filter(Post.id == post_id).first()
if not post:
raise HTTPException(status_code=404, detail="文章不存在")
return post
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.orm import Session
from database import get_db
from models import Post, Category
from schemas import PostResponse
router = APIRouter(
prefix="/posts", # 所有路由自动加上 /posts 前缀
tags=["文章"] # Swagger 文档中的分组标签
)
@router.get("/{post_id}", response_model=PostResponse, name="post_detail")
def post_detail(post_id: int, db: Session = Depends(get_db)):
"""文章详情(JSON API)"""
post = db.query(Post).filter(Post.id == post_id).first()
if not post:
raise HTTPException(status_code=404, detail="文章不存在")
return post
在 main.py 中挂载 APIRouter:
实例
from routers.posts import router as posts_router
from routers.categories import router as categories_router
app.include_router(posts_router)
app.include_router(categories_router)
from routers.categories import router as categories_router
app.include_router(posts_router)
app.include_router(categories_router)
渲染文章列表页
实例
# 文件路径:routers/posts.py
from fastapi import APIRouter, Depends, Request, Query
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from database import get_db
from models import Post, Category
router = APIRouter()
templates = Jinja2Templates(directory="templates")
@router.get("/", name="index")
def index(
request: Request,
category: str | None = Query(None), # 查询参数:可选
db: Session = Depends(get_db)
):
"""首页:文章列表 + 分类筛选"""
# 基础查询
posts_query = db.query(Post).order_by(Post.created_at.desc())
# 分类筛选
if category:
posts_query = posts_query.join(Category).filter(Category.slug == category)
posts = posts_query.all()
categories = db.query(Category).all()
return templates.TemplateResponse("index.html", {
"request": request,
"posts": posts,
"categories": categories,
"category_slug": category or ""
})
from fastapi import APIRouter, Depends, Request, Query
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from database import get_db
from models import Post, Category
router = APIRouter()
templates = Jinja2Templates(directory="templates")
@router.get("/", name="index")
def index(
request: Request,
category: str | None = Query(None), # 查询参数:可选
db: Session = Depends(get_db)
):
"""首页:文章列表 + 分类筛选"""
# 基础查询
posts_query = db.query(Post).order_by(Post.created_at.desc())
# 分类筛选
if category:
posts_query = posts_query.join(Category).filter(Category.slug == category)
posts = posts_query.all()
categories = db.query(Category).all()
return templates.TemplateResponse("index.html", {
"request": request,
"posts": posts,
"categories": categories,
"category_slug": category or ""
})
str | None = Query(None)是 Python 3.10+ 的类型联合语法,等价于Optional[str]。FastAPI 会根据类型提示自动:1. 使参数可选;2. 在 Swagger 文档中标记为可选;3. 不传时不报错。
渲染文章详情页
实例
# 文件路径:routers/posts.py 追加
@router.get("/post/{post_id}", name="post_detail")
def post_detail(request: Request, post_id: int, db: Session = Depends(get_db)):
"""文章详情页"""
post = db.query(Post).filter(Post.id == post_id).first()
if not post:
raise HTTPException(status_code=404, detail="文章不存在")
return templates.TemplateResponse("post_detail.html", {
"request": request,
"post": post
})
@router.get("/post/{post_id}", name="post_detail")
def post_detail(request: Request, post_id: int, db: Session = Depends(get_db)):
"""文章详情页"""
post = db.query(Post).filter(Post.id == post_id).first()
if not post:
raise HTTPException(status_code=404, detail="文章不存在")
return templates.TemplateResponse("post_detail.html", {
"request": request,
"post": post
})
注意区分:同一个路由可以同时提供 JSON API(声明 response_model)和 HTML 页面(返回 TemplateResponse)。通常的做法是:API 路由放在 /api/ 前缀下,页面路由放在根路径下。
创建首页和详情页模板
实例
<!-- 文件路径:templates/index.html -->
{% extends 'base.html' %}
{% block title %}RUNOOB 博客 - 首页{% endblock %}
{% block content %}
<h2 class="section-title">最新文章</h2>
<div class="category-bar">
<a href="/" class="{% if not category_slug %}active{% endif %}">全部</a>
{% for cat in categories %}
<a href="/?category={{ cat.slug }}"
class="{% if category_slug == cat.slug %}active{% endif %}">
{{ cat.name }}
</a>
{% endfor %}
</div>
{% if posts %}
<div class="article-grid">
{% for post in posts %}
<a href="/post/{{ post.id }}" class="card-link">
<div class="article-card">
<div class="card-content">
<span class="card-category">{{ post.category.name }}</span>
<h3>{{ post.title }}</h3>
<p>{{ post.summary|truncate(80) }}</p>
<span class="card-date">{{ post.created_at.strftime('%Y-%m-%d') }}</span>
</div>
</div>
</a>
{% endfor %}
</div>
{% else %}
<p class="empty-tip">该分类下暂无文章。</p>
{% endif %}
{% endblock %}
{% extends 'base.html' %}
{% block title %}RUNOOB 博客 - 首页{% endblock %}
{% block content %}
<h2 class="section-title">最新文章</h2>
<div class="category-bar">
<a href="/" class="{% if not category_slug %}active{% endif %}">全部</a>
{% for cat in categories %}
<a href="/?category={{ cat.slug }}"
class="{% if category_slug == cat.slug %}active{% endif %}">
{{ cat.name }}
</a>
{% endfor %}
</div>
{% if posts %}
<div class="article-grid">
{% for post in posts %}
<a href="/post/{{ post.id }}" class="card-link">
<div class="article-card">
<div class="card-content">
<span class="card-category">{{ post.category.name }}</span>
<h3>{{ post.title }}</h3>
<p>{{ post.summary|truncate(80) }}</p>
<span class="card-date">{{ post.created_at.strftime('%Y-%m-%d') }}</span>
</div>
</div>
</a>
{% endfor %}
</div>
{% else %}
<p class="empty-tip">该分类下暂无文章。</p>
{% endif %}
{% endblock %}
实例
<!-- 文件路径:templates/post_detail.html -->
{% extends 'base.html' %}
{% block title %}{{ post.title }} - RUNOOB 博客{% endblock %}
{% block content %}
<article class="post-view">
<span class="category-tag">{{ post.category.name }}</span>
<h1>{{ post.title }}</h1>
<time>{{ post.created_at.strftime('%Y-%m-%d') }}</time>
<div class="content">{{ post.content|safe }}</div>
<a href="/" class="back-link">← 返回首页</a>
</article>
{% endblock %}
{% extends 'base.html' %}
{% block title %}{{ post.title }} - RUNOOB 博客{% endblock %}
{% block content %}
<article class="post-view">
<span class="category-tag">{{ post.category.name }}</span>
<h1>{{ post.title }}</h1>
<time>{{ post.created_at.strftime('%Y-%m-%d') }}</time>
<div class="content">{{ post.content|safe }}</div>
<a href="/" class="back-link">← 返回首页</a>
</article>
{% endblock %}
本章小结
本章你完成了 FastAPI 的核心路由开发:APIRouter 按模块拆分路由(prefix + tags)、查询参数用 Query() + 类型提示声明、TemplateResponse 渲染页面、404 异常处理。
博客现在有了首页文章列表、分类筛选和详情页三个完整页面。
