前端转 AI 100 天 Day 21:FastAPI 入门——从命令行到 HTTP 服务

前端转 AI 第 21 天:FastAPI 入门 - 从命令行到 HTTP 服务

(明天国庆,假期期间不学习,博主要出去玩!!!假期后继续学习)

昨日回顾

Day 20 把助手打包成了 CLI 工具,能 pip install、能发布。

但有个问题------它只能在本地跑,别人用不了。

想分享给朋友?让他装 Python、装依赖、跑命令?门槛太高。

该上后端了。 今天开始 FastAPI,把助手变成一个 HTTP 服务,任何能发请求的地方都能用------网页、App、其他脚本、Postman。

Day 21 目标:搞懂 FastAPI 的核心,能写接口,把 Day 20 的助手改造成 HTTP 服务。


一、为什么选 FastAPI

Python 的 Web 框架不少,为什么 FastAPI 是AI 应用的首选?

1. 主流框架对比

框架 特点 类型 适合
Flask 极简、灵活 同步 小项目、传统 Web
Django 全家桶、重 同步 大型 CMS、后台
FastAPI 现代、异步、类型驱动 异步 API、AI 应用
Sanic 纯异步、快 异步 高并发
Tornado 老牌异步 异步 WebSocket

2. FastAPI 的三大优势

优势一:基于 Pydantic(Day 11 的成果直接用)

python 复制代码
from pydantic import BaseModel

class ChatRequest(BaseModel):
    message: str
    temperature: float = 0.7

@app.post("/chat")
def chat(req: ChatRequest):
    # req 自动校验、自动解析、自动补默认值
    ...

你只写数据模型,FastAPI 帮你做校验 + 解析 + 错误处理。

优势二:自动生成 API 文档

FastAPI 启动后自带两个文档界面:

  • /docs:Swagger UI(可交互)
  • /redoc:ReDoc(更清爽)

你一行文档都没写,它自动根据 Pydantic 模型和类型注解生成。 前端联调时直接给这个链接就行。

优势三:原生异步

python 复制代码
@app.get("/users/{user_id}")
async def get_user(user_id: int):
    user = await db.get_user(user_id)   # 直接 await
    return user

Agent 里大量 async 调用(LLM、向量库、HTTP),FastAPI 天生匹配。 Flask / Django 都是同步的,想做异步要额外折腾。

结论:做 AI 应用,没有理由不用 FastAPI。

3. 安装

bash 复制代码
pip install "fastapi[standard]"

[standard] 会连 uvicorn 等常用工具一起装上。


二、第一个 FastAPI 应用

1. Hello World

新建 day21/first_api/main.py:

python 复制代码
from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def read_root():
    return {"message": "Hello, FastAPI"}


@app.get("/health")
def health_check():
    return {"status": "ok"}

2. 启动

bash 复制代码
cd day21/first_api
uvicorn main:app --reload

参数解释:

  • main:app :main.py 文件里的 app 对象
  • --reload:改代码自动重启(开发模式)

输出:

vbnet 复制代码
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [12345]
INFO:     Started server process [12346]
INFO:     Application startup complete.

3. 测试

打开浏览器:

或者用 curl:

bash 复制代码
curl http://127.0.0.1:8000/

或前端习惯的写法:

js 复制代码
fetch("http://127.0.0.1:8000/").then(r => r.json()).then(console.log)

4. 见证奇迹:打开 /docs

访问 http://127.0.0.1:8000/docs

你会看到一个完整的、可交互的 Swagger UI:

  • 列出了所有接口
  • 每个接口都有"Try it out"按钮(可以直接在浏览器里测试)
  • 参数说明、返回结构、错误码全有

你一行文档都没写! 这就是 FastAPI 最爽的地方。

**再访问 http://127.0.0.1:8000/redoc**------另一个风格的文档界面。


三、路由与参数

1. 路径参数

python 复制代码
@app.get("/users/{user_id}")
def get_user(user_id: int):     # ← 声明 int,自动转换
    return {"user_id": user_id, "type": type(user_id).__name__}

访问 /users/123:

json 复制代码
{"user_id": 123, "type": "int"}

访问 /users/abc:

json 复制代码
{
  "detail": [
    {
      "loc": ["path", "user_id"],
      "msg": "Input should be a valid integer...",
      "type": "int_parsing"
    }
  ]
}

看到没?int 类型不匹配,FastAPI 自动返回 422 错误,还告诉你哪里错了。

前端视角: 相当于给每个参数都配了 zod 校验,还自动生成错误信息。

2. 查询参数

python 复制代码
@app.get("/search")
def search(
    q: str,
    limit: int = 10,
    sort: str = "desc",
):
    return {"q": q, "limit": limit, "sort": sort}

访问 /search?q=python&limit=5:

json 复制代码
{"q": "python", "limit": 5, "sort": "desc"}

规则:

  • 函数参数没在路径里 → FastAPI 自动识别为查询参数
  • 有默认值 → 可选参数
  • 没默认值 → 必填参数(不传报错)

3. 用 Query 加约束

python 复制代码
from fastapi import Query

@app.get("/items")
def list_items(
    q: str | None = Query(default=None, max_length=50, description="搜索关键词"),
    skip: int = Query(default=0, ge=0, description="跳过多少条"),
    limit: int = Query(default=10, ge=1, le=100, description="返回多少条"),
):
    return {"q": q, "skip": skip, "limit": limit}

ge / le / max_length 和 Day 11 的 Pydantic Field 一样------因为 FastAPI 就是用 Pydantic 做的。

description 会显示在 /docs 里。 前端联调时一目了然。

4. 路径参数 with 约束

python 复制代码
from fastapi import Path

@app.get("/users/{user_id}")
def get_user(
    user_id: int = Path(ge=1, description="用户 ID,正整数"),
):
    return {"user_id": user_id}

四、请求体:POST 的标配

这是 FastAPI 最强的地方------用 Pydantic 模型定义请求体。

python 复制代码
from pydantic import BaseModel, Field

class ChatMessage(BaseModel):
    role: str
    content: str

class ChatRequest(BaseModel):
    messages: list[ChatMessage]
    model: str = "gpt-4o-mini"
    temperature: float = Field(default=0.7, ge=0, le=2)
    max_tokens: int = Field(default=1024, ge=1, le=32000)


@app.post("/chat")
def chat(req: ChatRequest):
    # 到这里 req 已经被完整校验
    last = req.messages[-1]
    return {
        "reply": f"你说的是:{last.content}",
        "model": req.model,
        "tokens": 42,
    }

前端发请求:

js 复制代码
fetch("http://127.0.0.1:8000/chat", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    messages: [{ role: "user", content: "你好" }],
    temperature: 0.7,
  }),
});

你什么都没写,FastAPI 就做到了:

  • ✅ 校验 messages 非空
  • ✅ 校验 temperature 在 0-2 之间
  • ✅ 校验 max_tokens 是正整数
  • ✅ 自动补默认值
  • ✅ 类型转换("0.7" → 0.7)
  • ✅ 生成错误信息
  • ✅ 在 /docs 里展示结构

这就是 Day 11 说的"Pydantic 是 Agent 底牌"的最好体现。

1. 混合使用:路径 + 查询 + 请求体

python 复制代码
@app.put("/users/{user_id}")
def update_user(
    user_id: int,                        # 路径参数
    send_email: bool = False,            # 查询参数
    user: UserUpdate = ...,              # 请求体(Pydantic 模型)
):
    return {"user_id": user_id, "send_email": send_email, "user": user}

FastAPI 自动识别三种参数的位置------靠类型注解区分。

2. 单值请求体

如果请求体就是一个简单值(比如一个字符串),用 Body:

python 复制代码
from fastapi import Body

@app.post("/echo")
def echo(text: str = Body(embed=True)):
    return {"text": text}

请求体:

json 复制代码
{"text": "hello"}

embed=True 表示"包一层对象"。 不写的话请求体就是裸字符串 "hello"。


五、响应:response_model 和状态码

1. response_model:自动过滤敏感字段

场景: 用户对象有 password_hash,返回时不能带上。

python 复制代码
class UserInDB(BaseModel):
    id: int
    name: str
    password_hash: str    # 敏感


class UserOut(BaseModel):
    id: int
    name: str


@app.get("/users/{user_id}", response_model=UserOut)
def get_user(user_id: int) -> UserInDB:
    return UserInDB(id=1, name="张三", password_hash="xxx")

返回给客户端的只有 {"id": 1, "name": "张三"},password_hash 自动被过滤。

这个特性非常重要------防止不小心泄露字段。

2. 直接返回 Pydantic 模型

python 复制代码
@app.get("/user", response_model=UserOut)
def get_user():
    return UserOut(id=1, name="张三")   # FastAPI 自动转 JSON

和返回 dict 一样,但类型更明确。

3. 状态码

python 复制代码
from fastapi import status

@app.post("/users", status_code=status.HTTP_201_CREATED)
def create_user(user: UserIn):
    ...
    return {"id": 1}

常用状态码:

状态码 含义
200 成功(默认)
201 创建成功
204 成功但无内容(删除)
400 客户端错误
401 未认证
403 无权限
404 找不到
422 参数校验失败(FastAPI 自动用)
500 服务器错误

4. 抛异常:HTTPException

python 复制代码
from fastapi import HTTPException

@app.get("/users/{user_id}")
def get_user(user_id: int):
    user = db.get(user_id)
    if not user:
        raise HTTPException(
            status_code=404,
            detail=f"用户 {user_id} 不存在",
        )
    return user

客户端收到:

json 复制代码
{"detail": "用户 123 不存在"}

HTTPException 会自动转成 JSON 响应 + 状态码。


六、中间件和生命周期

1. CORS:前端联调必配

前端在 localhost:5173 起服务,后端在 localhost:8000------浏览器会拦。

python 复制代码
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173"],   # 前端地址
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

开发阶段可以先 allow_origins=["*"] 方便调试,生产环境再收紧。

这个问题前端同学最熟------浏览器跨域,后端加 CORS 头就能解决。

2. 启动和关闭:lifespan

FastAPI 推荐用 lifespan 管理资源(数据库连接、LLM 客户端):

python 复制代码
from contextlib import asynccontextmanager
from fastapi import FastAPI


@asynccontextmanager
async def lifespan(app: FastAPI):
    # 启动时执行
    print("启动:初始化数据库")
    app.state.db = init_database()

    yield    # ← 应用运行期间

    # 关闭时执行
    print("关闭:清理资源")
    app.state.db.close()


app = FastAPI(lifespan=lifespan)


@app.get("/")
def root():
    db = app.state.db   # 从 app.state 拿
    return {"status": "ok"}

好处:

  • 一次性初始化:不用每个请求都建连接
  • 优雅关闭:进程退出前能清理资源
  • 可测试 :可以在测试里替换 app.state.db

Day 23 会详细讲这套模式。


七、实战:把助手改造成 HTTP 服务

把 Day 20 的命令行助手改造成 HTTP API。

1. 项目结构

bash 复制代码
cp -r day20/ai-assistant-v7 day21/ai-assistant-api
cd day21/ai-assistant-api

目录调整:

bash 复制代码
src/ai_assistant/
├── __init__.py
├── main.py               # CLI 入口(保留)
├── api/
│   ├── __init__.py
│   ├── app.py            # ← FastAPI 实例(新增)
│   ├── routes.py         # ← 路由定义(新增)
│   ├── schemas.py        # ← API 请求/响应模型(新增)
│   └── deps.py           # ← 依赖注入(新增)
├── config.py
├── models.py
├── orm_models.py
├── db_orm.py
├── storage_orm.py
├── llm.py
├── memory.py
└── ...

新增 api/ 子包,与 CLI 层解耦。

2. api/schemas.py:API 请求响应模型

python 复制代码
"""API 请求/响应模型"""
from pydantic import BaseModel, Field
from datetime import datetime
from typing import Literal


Role = Literal["system", "user", "assistant"]


class MessageSchema(BaseModel):
    role: Role
    content: str = Field(min_length=1, max_length=10000)


class ChatRequest(BaseModel):
    """对话请求"""
    message: str = Field(min_length=1, max_length=10000, description="用户消息")
    session_id: str | None = Field(default=None, description="会话 ID,不传则创建新会话")


class ChatResponse(BaseModel):
    """对话响应"""
    session_id: str
    reply: str
    message_count: int


class SessionInfo(BaseModel):
    """会话信息"""
    id: str
    title: str
    updated_at: datetime
    message_count: int


class MessageInfo(BaseModel):
    """消息信息"""
    role: Role
    content: str
    timestamp: datetime


class SessionDetail(BaseModel):
    """会话详情"""
    id: str
    title: str
    messages: list[MessageInfo]
    created_at: datetime
    updated_at: datetime


class SessionListResponse(BaseModel):
    """会话列表响应"""
    sessions: list[SessionInfo]
    total: int

关键点:这些模型和 Day 14 的业务模型不同。

  • 业务模型 (models.py):内部用
  • API 模型 (schemas.py):对外用

为什么要分开?

  • 业务模型可能变化:内部字段调整不影响 API
  • API 模型有约束 :max_length / description 是给前端看的
  • 可以隐藏字段:内部字段不对 API 暴露

这是"API 契约"的思想。 前端联调时看 /docs 就够了,不用读你源码。

3. api/deps.py:依赖注入

FastAPI 的"依赖注入"是一大特色------把"每次请求都需要的东西"抽出来复用。

python 复制代码
"""依赖注入"""
from fastapi import Depends
from functools import lru_cache

from ..config import config
from ..db_orm import DatabaseORM
from ..storage_orm import SessionStorageORM
from ..llm import MockLLMClient
from ..memory import MemoryManager


@lru_cache
def get_db() -> DatabaseORM:
    """全局唯一的数据库连接"""
    return DatabaseORM(config.DATA_DIR / "assistant.db")


@lru_cache
def get_storage() -> SessionStorageORM:
    return SessionStorageORM(get_db())


def get_llm() -> MockLLMClient:
    return MockLLMClient()


def get_memory() -> MemoryManager:
    return MemoryManager(
        max_context_tokens=8000,
        recent_window=10,
        summary_threshold=20,
    )

lru_cache 保证 get_db() 只执行一次------第一次调用建连接,后续直接返回同一个对象。

用法:

python 复制代码
@app.get("/sessions")
def list_sessions(storage: SessionStorageORM = Depends(get_storage)):
    return storage.list_sessions()

Depends(get_storage) 告诉 FastAPI:

这个接口需要 storage,帮我调用 get_storage() 拿一个。

好处:

  • 不用手动传参
  • 可测试 :测试时替换 get_storage 就行
  • 可复用:多个接口共享同一个依赖

前端类比: 有点像 React 的 Context,或者 NestJS 的 @Injectable()。

4. api/routes.py:路由定义

python 复制代码
"""API 路由"""
from fastapi import APIRouter, HTTPException, Depends

from ..models import Session as SessionModel, Message
from ..llm import LLMError
from ..storage_orm import SessionStorageORM
from .schemas import (
    ChatRequest, ChatResponse,
    SessionInfo, SessionListResponse,
    SessionDetail, MessageInfo,
)
from .deps import get_storage, get_llm, get_memory


router = APIRouter(prefix="/api", tags=["assistant"])


# ============ 健康检查 ============

@router.get("/health")
def health():
    return {"status": "ok"}


# ============ 对话 ============

@router.post("/chat", response_model=ChatResponse)
async def chat(
    req: ChatRequest,
    storage: SessionStorageORM = Depends(get_storage),
    llm = Depends(get_llm),
    memory = Depends(get_memory),
):
    """
    发送一条消息,返回助手的回复
    
    - 如果传了 session_id,就在该会话里继续对话
    - 如果不传,会创建一个新会话
    """
    # 1. 加载或创建会话
    if req.session_id:
        session = storage.load(req.session_id)
        if not session:
            raise HTTPException(status_code=404, detail=f"会话不存在: {req.session_id}")
    else:
        import uuid
        session = SessionModel(session_id=uuid.uuid4().hex[:8])

    # 2. 加用户消息
    session.add_message("user", req.message)

    # 3. 组装上下文
    context = memory.get_context(session.messages)
    messages = [{"role": "system", "content": "你是一个友好的 AI 助手"}]
    messages.extend(m.to_llm_format() for m in context)

    # 4. 调 LLM
    try:
        reply = await llm.chat(messages)
    except LLMError as e:
        raise HTTPException(status_code=503, detail=f"模型调用失败: {e}")

    # 5. 加助手回复 + 保存
    session.add_message("assistant", reply)
    storage.save(session)

    return ChatResponse(
        session_id=session.session_id,
        reply=reply,
        message_count=session.message_count,
    )


# ============ 会话管理 ============

@router.get("/sessions", response_model=SessionListResponse)
def list_sessions(storage: SessionStorageORM = Depends(get_storage)):
    """列出所有会话"""
    sessions = storage.list_sessions()
    return SessionListResponse(
        sessions=[SessionInfo(**s) for s in sessions],
        total=len(sessions),
    )


@router.get("/sessions/{session_id}", response_model=SessionDetail)
def get_session(session_id: str, storage: SessionStorageORM = Depends(get_storage)):
    """获取会话详情"""
    session = storage.load(session_id)
    if not session:
        raise HTTPException(status_code=404, detail="会话不存在")

    return SessionDetail(
        id=session.session_id,
        title=session.title,
        messages=[
            MessageInfo(role=m.role, content=m.content, timestamp=m.timestamp)
            for m in session.messages
        ],
        created_at=session.created_at,
        updated_at=session.updated_at,
    )


@router.delete("/sessions/{session_id}", status_code=204)
def delete_session(session_id: str, storage: SessionStorageORM = Depends(get_storage)):
    """删除会话"""
    if not storage.delete(session_id):
        raise HTTPException(status_code=404, detail="会话不存在")


@router.get("/search")
def search_sessions(
    q: str,
    limit: int = 20,
    storage: SessionStorageORM = Depends(get_storage),
):
    """搜索包含关键字的会话"""
    results = storage.search(q, limit=limit)
    return {"keyword": q, "results": results, "count": len(results)}

看几个关键设计:

1. APIRouter 统一前缀和标签

python 复制代码
router = APIRouter(prefix="/api", tags=["assistant"])

所有路由自动加 /api 前缀 ,/docs 里分组显示。

2. Depends 拿依赖

python 复制代码
storage: SessionStorageORM = Depends(get_storage)

不用手动 storage = get_storage(),FastAPI 帮你注入。 而且同一个请求里多次 Depends(get_storage) 会拿到同一个实例(有缓存机制)。

3. 业务异常转 HTTP 异常

python 复制代码
if not session:
    raise HTTPException(status_code=404, detail="会话不存在")

内部业务逻辑不关心 HTTP 状态码,路由层负责转换。

这是"分层架构"的体现------业务层不知道 HTTP 的存在。

4. 异步 + 同步混用

python 复制代码
@router.post("/chat", ...)
async def chat(...):   # 因为内部有 await llm.chat,所以用 async
    ...

@router.get("/sessions", ...)
def list_sessions(...):   # 纯数据库操作,用 def 就行(FastAPI 会丢到线程池)
    ...

规则:

  • 内部有 await → 用 async def
  • 纯同步代码 (数据库、CPU 计算) → 用 def,FastAPI 会自动放线程池

别把所有接口都写成 async ------同步代码在 async def 里会阻塞事件循环。

5. api/app.py:FastAPI 实例

python 复制代码
"""FastAPI 应用"""
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from ..logger import get_logger
from .routes import router
from .deps import get_db


logger = get_logger(__name__)


@asynccontextmanager
async def lifespan(app: FastAPI):
    """启动/关闭钩子"""
    logger.info("API 启动")
    db = get_db()
    # 数据库已在 init 时建表,这里不需要额外操作
    yield
    logger.info("API 关闭")
    db.close()


def create_app() -> FastAPI:
    """应用工厂"""
    app = FastAPI(
        title="AI 助手 API",
        description="前端转 AI 学习项目的后端服务",
        version="0.1.0",
        lifespan=lifespan,
    )

    # CORS
    app.add_middleware(
        CORSMiddleware,
        allow_origins=["*"],   # 开发阶段放开,生产收紧
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )

    # 挂路由
    app.include_router(router)

    return app


app = create_app()

关键点:

1. lifespan 管理资源

启动时 :初始化数据库;关闭时:清理资源。

2. create_app() 应用工厂

为什么用工厂函数,而不是直接 app = FastAPI()?

  • 可测试:测试时可以传不同配置
  • 可多实例:某些场景需要创建多个 app
  • 更规范:社区推荐模式

3. app.include_router()

把路由挂到 app 上。 后续加新模块(比如 /api/tools)就再写一个 router,include 进来就行。

6. 启动 API

在 pyproject.toml 里加一个命令:

toml 复制代码
[project.scripts]
ai-assistant = "ai_assistant.main:main"
ai-assistant-api = "ai_assistant.api.run:main"

创建 api/run.py:

python 复制代码
"""API 启动入口"""
import uvicorn
from ..logger import setup_logging


def main():
    setup_logging("INFO")
    uvicorn.run(
        "ai_assistant.api.app:app",
        host="0.0.0.0",
        port=8000,
        reload=True,     # 开发模式
    )


if __name__ == "__main__":
    main()

运行:

bash 复制代码
pip install -e .
ai-assistant-api

或直接:

bash 复制代码
uvicorn ai_assistant.api.app:app --reload

访问 http://localhost:8000/docs,看到完整文档。

7. 测试整个流程

用 curl 或 Postman 测试:

1. 发一条消息(不传 session_id):

bash 复制代码
curl -X POST http://localhost:8000/api/chat \
  -H "Content-Type: application/json" \
  -d '{"message": "你好"}'

响应:

json 复制代码
{
  "session_id": "a3f9b2e1",
  "reply": "你好!我是一个模拟的 AI 助手...",
  "message_count": 2
}

2. 继续这个会话:

bash 复制代码
curl -X POST http://localhost:8000/api/chat \
  -H "Content-Type: application/json" \
  -d '{"message": "Python 是什么", "session_id": "a3f9b2e1"}'

3. 列出所有会话:

bash 复制代码
curl http://localhost:8000/api/sessions

4. 获取会话详情:

bash 复制代码
curl http://localhost:8000/api/sessions/a3f9b2e1

5. 删除会话:

bash 复制代码
curl -X DELETE http://localhost:8000/api/sessions/a3f9b2e1

6. 搜索:

bash 复制代码
curl "http://localhost:8000/api/search?q=Python"

8. 前端调用示范

前端拿到 API 后,fetch 就好:

js 复制代码
const API_BASE = "http://localhost:8000/api";

async function chat(message, sessionId = null) {
  const res = await fetch(`${API_BASE}/chat`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ message, session_id: sessionId }),
  });

  if (!res.ok) {
    const err = await res.json();
    throw new Error(err.detail);
  }

  return res.json();
}

// 使用
const result = await chat("你好");
console.log(result.reply);

你前端的 fetch 代码,一行不用改------FastAPI 返回的就是标准 JSON + 状态码。

9. 从 curl 到前端,全链路打通

你现在有了一个完整的 AI 助手服务:

bash 复制代码
┌─────────────┐
│  前端页面   │  ← fetch / axios
│  (React)    │
└──────┬──────┘
       │ HTTP
       ▼
┌─────────────┐
│  FastAPI    │  ← /api/chat /api/sessions
└──────┬──────┘
       │
       ▼
┌─────────────┐
│  业务层     │  ← SessionStorageORM / MemoryManager
└──────┬──────┘
       │
       ▼
┌─────────────┐
│  SQLite     │
└─────────────┘

Day 46 接真 LLM 时,只需要改 llm.py。 API 层、业务层、存储层全不动。

这就是 Day 13 说的"架构好,改一处不影响全局"。


八、前端视角:FastAPI vs Express/Koa

做过前端的你,会觉得 FastAPI 极其顺手。

概念 Express FastAPI
应用实例 const app = express() app = FastAPI()
定义路由 app.get("/", handler) @app.get("/") 装饰器
路径参数 req.params.id 函数参数 user_id: int
查询参数 req.query.q 函数参数 q: str
请求体 req.body(要 body-parser) Pydantic 模型(自动)
校验 手写或 zod Pydantic(内置)
错误处理 res.status(404).json(...) raise HTTPException(404, ...)
中间件 app.use(...) app.add_middleware(...)
自动文档 装 swagger-jsdoc 内置
类型安全 TypeScript Python 类型注解

核心差异:

1. FastAPI 是"类型驱动"的

Express 里你得手写校验:

js 复制代码
app.post("/chat", (req, res) => {
  const { message } = req.body;
  if (!message || typeof message !== "string") {
    return res.status(400).json({ error: "message required" });
  }
  // ...
});

FastAPI 里:

python 复制代码
@app.post("/chat")
def chat(req: ChatRequest):
    # 校验已经做完了
    ...

2. FastAPI 自带文档

Express 要装 swagger-jsdoc 或 tsoa 才能有文档。 FastAPI 天然有。

3. 异步语法几乎一样

Express:

js 复制代码
app.get("/users/:id", async (req, res) => {
  const user = await db.getUser(req.params.id);
  res.json(user);
});

FastAPI:

python 复制代码
@app.get("/users/{user_id}")
async def get_user(user_id: int):
    user = await db.get_user(user_id)
    return user

一一对应,几乎无痛迁移。


九、今日踩坑

  1. uvicorn main:app 找不到模块 → 要在项目根目录执行,或者装成包 pip install -e .
  2. @app.get 忘了写 → 路由没注册,404
  3. 所有接口都写 async def → 同步代码在 async def 里会阻塞事件循环,纯同步用 def
  4. Pydantic 模型和业务模型混用 → 分开定义,API 契约更清晰
  5. Depends 里用了 @lru_cache 但对象不该全局 → 有状态的(如请求级 Session)不能用 lru_cache
  6. CORS 没配 → 前端联调一堆跨域错误
  7. response_model 忘了加 → 敏感字段泄露,比如密码 hash
  8. raise HTTPException 写成了 return → 状态码不对
  9. async def 里调同步库 (requests / time.sleep) → 阻塞事件循环,要用 asyncio.to_thread 或换异步库
  10. --reload 上了生产 → 生产环境要关掉,性能和安全都受影响

十、今日总结

今天核心收获三件事:

  1. FastAPI = 类型驱动 + Pydantic + 异步,是 AI 应用后端的最优选择
  2. @app.get / @app.post + Pydantic 模型,接口定义清晰,自动校验、自动文档
  3. Depends 依赖注入 + lifespan 生命周期,是 FastAPI 的"高级特性",也是工程化关键

一个感受:

前端的"接口对接"是一个边界,FastAPI 把这条边界变得极其清晰。

你写 Pydantic 模型,前端看 /docs,双方都不需要看对方源码。

这就是 FastAPI 的最大价值------它把"API 契约"从文档约定变成了代码约束。

你的项目现在是一个真正的"AI 助手服务"了。

CLI 能跑,HTTP 能调,前端能连,测试覆盖,可分发包。

从"脚本"到"工具"到"服务",一个完整的软件进化链走完了。

Day 21 完成度:✅

明日预告(Day 22)

  • FastAPI 进阶 :
    • 依赖注入的更多用法(带参数、类依赖)
    • 中间件:请求日志、耗时统计
    • 异常处理器:统一错误格式
    • 路由分组与嵌套
  • 认证入门:API Key 校验
  • 给 API 加一套完整的中间件栈

最后

如果这篇对你有用,点个赞让我知道有人在看。

评论区互动: 你之前写后端用什么?Express / Koa / Flask / Django?和 FastAPI 比怎么样?

有问题评论区见,我们国庆假期结束后继续。

100 天,国庆假期结束后见。


附录:系列目录

已完成 ✅

阶段 天数 内容
基础 01-06 Python 语法、数据结构、函数、OOP、文件、依赖管理
网络 07-08 HTTP 请求、异步 asyncio
工程 09-12 正则、装饰器、Pydantic、日志调试
项目 13-14 命令行 AI 助手 v1 / v2
复盘 15 阶段总结
数据 16-17 SQLite 数据库、SQLAlchemy ORM
测试 18-19 pytest 入门 + 进阶
工具 20 CLI 打包与发布
后端 21 FastAPI 入门 ✅

进行中 🎯

阶段 天数 内容
后端 22-30 FastAPI 进阶 + 完整服务端
大模型 31-45 LLM API + Prompt 工程
RAG 46-60 向量数据库 + 知识库
Agent 61-80 LangChain / 工具调用
实战 81-100 完整 Agent 项目上线

100 天后见,一起从切图仔变成 Agent 开发者。

相关推荐
QQ14220784491 小时前
基于 Flask 的「自动任务控制台」设计与踩坑实录
后端·python·flask
吴佳浩 Alben1 小时前
一文讲透推理引擎性能指标:TTFT、TPOT、KV命中率、并发,到底对应 vLLM / SGLang 的哪个参数?
人工智能·python·语言模型·架构·ai编程·vllm·sglang
用户298698530141 小时前
Python 实现 Word 转 XPS、PostScript 与 OFD 格式
后端·python·api
Patrick在香港1 小时前
你写的「比较 token」函数,正在用 36 纳秒泄露密码:时序攻击实测
python·安全·标准库·时序攻击·常量时间
蜗牛互联网1 小时前
Python Agent 工具调用:审批令牌、参数白名单和累计预算
java·开发语言·人工智能·windows·后端·python
维克兜率天2 小时前
【维克】统计套利:寻找市场的“定价误差“
人工智能·python·算法
承渊政道2 小时前
【Python编程—从入门到实践】(让程序记得、恢复并解释失败:Python 文件、异常与JSON系统教程)
开发语言·python·pycharm·json·异常处理
郭庆汝2 小时前
(六)MCP+A2A之——Playwright与BrowserUse:让AI学会像人类一样使用浏览器
开发语言·python
Yolanda_20222 小时前
26.网络模型的保存与读取
人工智能·pytorch·python