从 JS/TS 到 Python:前端开发者的无缝迁移指南(二) 服务篇

一、环境与工具链:从 nvm/npm 到 pyenv/poetry

Python 的包管理和环境隔离体系,是 Node 开发者迁移时最容易”撞墙”的地方——不是因为难,而是因为默认行为和 Node 生态完全不同。Node 的 node_modules 天然提供项目级隔离,而 Python 的 pip install 是全局安装。这一节的目标是让你在 10 分钟内搞清楚这个体系,避开最痛的坑。

1.1 版本管理:nvm → pyenv

Node 开发者对 nvm/fnm 的使用已经刻在肌肉记忆里。Python 界有对应物:pyenv

特性/概念 JS/TS Python 核心差异与避坑说明
版本管理器 nvm / fnm / volta pyenv (Windows 用 pyenv-win) pyenv 需要编译安装 CPython,首次 pyenv install 3.12 会从源码编译,耗时较长
版本声明文件 .nvmrc .python-version 语法相同,都是纯文本写版本号
安装指定版本 nvm install 20 pyenv install 3.12 pyenv 安装后还需 pip install 各种工具
查看已安装版本 nvm ls pyenv versions
全局默认版本 nvm alias default 20 pyenv global 3.12
终端自动切换 .nvmrc + nvm 脚本 .python-version + pyenv 自动读取 无需额外配置,进入目录自动切换

⚠️ 避坑警告:macOS/Linux 上不要用系统自带的 Python(/usr/bin/python3)。那个版本通常偏旧,且是系统工具链的依赖,乱动可能搞崩系统。务必通过 pyenv 安装独立版本。

// Node: 项目级版本锁定
// $ echo "20" > .nvmrc
// $ nvm use
# Python: 项目级版本锁定
# $ echo "3.12" > .python-version
# $ pyenv local 3.12  # 或直接 cd 进入目录自动切换

1.2 虚拟环境:node_modules 的天然隔离 vs 必须手动创建

这是 Node 开发者最容易踩的坑。在 Node 里,npm install 默认安装在当前项目的 node_modules 下,项目之间天然隔离。Python 的 pip install 默认装到全局

特性/概念 JS/TS Python
项目级依赖隔离 node_modules(自动,无需任何操作) venv(虚拟环境,必须手动创建和激活)
创建隔离环境 不需要,npm init 就是项目边界 python -m venv .venv
激活环境 不需要 source .venv/bin/activate(macOS/Linux)或 .venv\Scripts\activate(Windows)
环境标识 package.json 的存在即为项目根 .venv/ 目录
退出环境 离开目录即退出 deactivate
依赖全局安装 npm install -g xxx pip install xxx这也是全局安装,别搞混
// Node:进入项目目录后直接装依赖,天然隔离
// $ cd my-project && npm install
// 依赖全在 node_modules 里,互不影响
# Python:必须先创建并激活虚拟环境
# $ python -m venv .venv
# $ source .venv/bin/activate   # Linux/macOS
# $ .venv\Scripts\activate      # Windows
# (.venv) $ pip install fastapi  # 注意终端提示符变了

⚠️ 头号避坑:你忘记激活 venv 时,pip install 不会报错——它只是默默装到了全局。然后你会困惑”为什么项目中 import 不到?”。解决方案:在 setting.json 中设置 "python.terminal.activateEnvironment": true,让 VS Code 自动激活 venv。也可以在项目根目录加一个 .envrc 配合 direnv 自动激活。

1.3 包管理器演进:npm/yarn/pnpm → pip/poetry/uv

这可能是 Node 开发者在 Python 生态里最怀念的一个功能——统一的项目配置文件

特性/概念 JS/TS Python
项目配置文件 package.json pyproject.toml(现代标准)/ setup.py(遗留)
默认包管理器 npm pip(只是安装器,不管理项目元数据)
Lock 文件 package-lock.json / yarn.lock / pnpm-lock.yaml poetry.lock / uv.lock(pip 没有内置 lock)
现代包管理器 pnpm(快、磁盘省) uv(Rust 实现,速度对标 pnpm)
依赖分组 dependencies / devDependencies [tool.poetry.dependencies] / [tool.poetry.group.dev.dependencies]
Scripts npm run dev 无官方等价物,通常用 make / task / just
Workspace pnpm-workspace.yaml pyproject.toml[tool.uv.sources]
// Node:npm/yarn/pnpm 自带 scripts 能力
// "scripts": { "dev": "nodemon src/index.ts" }
// $ npm run dev
# Python:没有内置的 npm scripts 等价物
# 多数项目直接用 Makefile 或 task runner
# Makefile 示例:
# dev:
# 	uvicorn app.main:app --reload
# $ make dev

推荐方案:新项目直接用 uv。它是 Rust 写的,速度碾压 pip,且整合了包管理和虚拟环境管理。uv inituv add fastapiuv run uvicorn app.main:app 一条龙,工作流上最贴近 pnpm 的体验。

1.4 安装依赖对照速查

操作 JS/TS Python (pip) Python (uv,推荐)
安装项目所有依赖 npm install pip install -r requirements.txt uv sync
安装单个包 npm install fastify pip install fastapi uv add fastapi
安装开发依赖 npm install -D typescript pip install -r requirements-dev.txt uv add --dev pytest
卸载包 npm uninstall axios pip uninstall requests uv remove requests
查看已安装 npm ls pip list uv pip list
升级包 npm update pip install --upgrade xxx uv sync --upgrade
导出依赖 不必要(package.json 即规范) pip freeze > requirements.txt uv export
// Node 的安装流程
// $ npm init
// $ npm install express
// $ npm install -D @types/express
# Python + uv 的等效流程
# $ uv init my-project
# $ uv add fastapi
# $ uv add --dev pytest
# $ uv run python -m app.main   # 运行项目

1.5 环境变量与配置管理

特性/概念 JS/TS Python
读取环境变量 process.env.PORT os.environ["PORT"]
.env 文件加载 dotenv python-dotenv
带验证的配置 zod + process.env pydantic-settings
安全访问(不存在时报错) process.env.PORT ?? 3000 os.environ.get("PORT", 3000)
// TypeScript: zod 验证环境变量
import { z } from "zod";

const env = z.object({
  PORT: z.coerce.number().default(3000),
  DATABASE_URL: z.string(),
}).parse(process.env);
# Python: pydantic-settings 验证环境变量
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    port: int = 3000
    database_url: str

    model_config = {"env_prefix": "", "env_file": ".env"}

settings = Settings()  # 自动读取 .env 并验证类型

1.6 这一节你该记住的

  1. pyenv 管 Python 版本,venv 管依赖隔离——任何项目第一步 python -m venv .venv && source .venv/bin/activate
  2. pip 是旧世界的工具,uv 是新世界的工具——uvpnpm 之于 npm
  3. Python 没有 npm scripts——用 Makefile 或 just 代替
  4. pyproject.toml = package.json——但不完全等价,它只声明元数据和依赖,不负责脚本和运行

二、Web 框架全景:Express/Koa/Fastify 用户该选什么

Python Web 框架的格局和 Node 没有本质区别——也是”微框架 → 全栈框架”的梯度分布。关键是找到你熟悉框架的对应物,避免用 Express 的惯性去用 Django。

2.1 三大框架速览

框架 定位 Node 对照 特点
Flask 微框架,最小核心 + 插件生态 Express 自由度高,全家桶靠第三方库拼装
FastAPI 现代异步框架,类型驱动 Fastify / Koa + TypeScript 原生 async、自动生成 OpenAPI、Pydantic 集成
Django 全栈电池框架 Nest.js / Adonis.js ORM、Admin 后台、认证、模板引擎全内置

2.2 Flask ≈ Express:极简哲学,插件拼装

Flask 和 Express 的设计思想几乎完全一致——核心极简,功能通过扩展补全:

功能 Express Flask
路由 app.get('/users', handler) @app.route("/users")
请求体解析 express.json() 中间件 request.get_json()
参数校验 Joi / zod(自己集成) marshmallow / pydantic(自己集成)
CORS cors() 中间件 flask-cors 扩展
数据库 自己选 ORM flask-sqlalchemy 扩展
模板引擎 EJS / Pug Jinja2(内置,和 Nunjucks 同源)
// Express: 最小化 API
const express = require("express");
const app = express();

app.get("/hello", (req, res) => {
  res.json({ message: `Hello, ${req.query.name}` });
});

app.listen(3000);
# Flask: 最小化 API——结构和思路完全一致
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route("/hello")
def hello():
    name = request.args.get("name")
    return jsonify({"message": f"Hello, {name}"})

app.run(port=3000)

⚠️ 避坑警告:Flask 对 async/await 的支持很有限。@app.route 装饰的函数默认是同步的,虽然 Flask 2.0+ 可以在异步视图上写 async def,但 Flask 本身是同步框架——异步视图在单独的线程中运行,性能提升有限。如果你的项目重度依赖 async(数据库查询、HTTP 调用),请直接选 FastAPI,别用 Flask。

2.3 FastAPI ≈ Fastify + TypeScript:现代化、高性能

FastAPI 是对 Node 开发者最友好的 Python 框架。如果你习惯写 TypeScript + Fastify + zod/json-schema,你会感觉 FastAPI 就像一个 “Python 版的 Fastify”:

特性 Fastify FastAPI
核心异步 ✅ 原生 Promise/async ✅ 原生 async/await
请求验证 JSON Schema / zod Pydantic(自动生成 JSON Schema)
自动文档 Swagger(需配置 fastify-swagger) Swagger + ReDoc 零配置自动生成
序列化 手动 or ajv Pydantic 自动序列化
依赖注入 无内置 Depends() 依赖注入系统
性能基准 高(Node 顶级) 高(Python 顶级,可与 Node 框架对打)
// Fastify + TypeScript: 类型驱动的路由
import fastify from "fastify";

const app = fastify();

app.get<{
  Querystring: { name: string };
}>("/hello", async (req) => {
  return { message: `Hello, ${req.query.name}` };
});

await app.listen({ port: 3000 });
# FastAPI: 类型驱动的路由——几乎一模一样的思路
from fastapi import FastAPI

app = FastAPI()

@app.get("/hello")
async def hello(name: str):      # 查询参数类型声明即验证
    return {"message": f"Hello, {name}"}

# uvicorn 启动: $ uvicorn main:app --reload

2.4 Django ≈ Nest.js:全栈”电池框架”

如果你来自 Nest.js 或 Angular 背景,Django 的哲学你会觉得眼熟——模块化、依赖注入、装饰器、ORM 全内置。但 Django 的笨重体量和学习曲线对于一个只需要写 API 的 Node 开发者来说通常是过度的:

特性 Nest.js Django
架构 Module → Controller → Service App → View → Model
ORM TypeORM / Prisma Django ORM(内置)
管理后台 无内置 Django Admin(零代码生成 CRUD 界面
模板引擎 Handlebars / 可选 Django Templates(内置)
认证 Passport.js / @nestjs/passport django.contrib.auth(内置)
迁移 TypeORM Migrations / Drizzle Kit Django Migrations(内置)
脚手架 nest g resource python manage.py startapp

Django 的唯一优势在于开发全栈单体应用极快——Admin 后台、ORM、表单验证、认证、模板全内置。但如果你只是写 API,Django 的大量内置功能都是噪音。写 API 首选 FastAPI,写全栈后台用 Django。

2.5 选型速查

你的背景 推荐框架 理由
Express + 少量 TypeScript Flask 学习曲线最低,概念最接近
Fastify + TypeScript + zod FastAPI(强烈推荐) 开发体验最接近,类型驱动,自动文档
Nest.js 用户写全栈应用 Django 全内置方案,无需拼装第三方库
只想写高性能 API,不关心全栈 FastAPI 异步原生、性能最优、文档自动生成
写边缘小脚本或仅暴露几个端点 Flask 够用,无过度设计

2.6 这一节你该记住的

  1. 写 API 就 FastAPI——类型驱动、自动文档、性能好,对 TypeScript 用户最友好
  2. Flask = Python 界的 Express——概念一样,但别指望它写高性能异步服务
  3. Django 的 Admin 是魔法——但这个魔法对纯 API 项目来说是负担

三、FastAPI 实战:从零搭建一个 API 服务

这一节我们不谈理论,用 Fastify + TypeScript 用户的视角,一步步写出一个完整的 CRUD API 服务。目标:看完你就能上手写。

3.1 项目初始化

# Python 项目初始化(uv 推荐)
$ uv init fastapi-blog
$ cd fastapi-blog
$ uv add fastapi uvicorn
$ uv add --dev pytest httpx

# 等效于 Node:
# $ mkdir fastapi-blog && cd fastapi-blog && npm init -y
# $ npm install fastapi  # 注意:Python 版叫 fastapi,是 pip 包
# $ npm install -D jest supertest

推荐目录结构:

# Node
src/
  routes/
    posts.ts
  models/
    post.ts
  middleware/
    auth.ts
  index.ts

# Python
app/
  routers/
    posts.py
  models/
    post.py
  dependencies/
    auth.py
  main.py

3.2 路由与请求处理

这是 Node 开发者最直接的切入点——写路由。

操作 Fastify FastAPI
GET 路由 app.get("/path", handler) @app.get("/path")
路径参数 "/users/:id" "/users/{id}"
查询参数 req.query.page 函数参数 page: int = 1
请求体 req.body 函数参数 body: SomeModel
请求头 req.headers.authorization Header(None)
Cookie req.cookies Cookie(None)
响应状态码 res.status(201).send(...) return ..., status_code=201@app.post(status_code=201)
// Fastify: 多参数来源的路由
app.get<{
  Params: { id: string };
  Querystring: { include_comments: boolean };
  Headers: { authorization: string };
}>("/posts/:id", async (req) => {
  const { id } = req.params;
  const { include_comments } = req.query;
  const token = req.headers.authorization;
  // ...
});
# FastAPI: 参数来源由声明位置自动推断——路径参数在装饰器路径中,其他在函数签名中
from fastapi import FastAPI, Header

app = FastAPI()

@app.get("/posts/{id}")
async def get_post(
    id: int,                              # 路径参数(装饰器中同名)
    include_comments: bool = False,       # 查询参数
    authorization: str = Header(None),    # 请求头
):
    return {"id": id, "include_comments": include_comments, "auth": authorization}

核心理解:FastAPI 通过函数签名中的参数名和类型注解自动推断参数来源。路径中 {id} 的同名参数自动绑定为路径参数,未被路径或 Header()/Cookie() 包裹的参数默认是查询参数。

3.3 请求体验证:zod → Pydantic

在 Node 里你用 zod/json-schema 校验请求体,FastAPI 把这一步变成了 Pydantic 类型声明——声明即校验,手动挡变自动挡。

// Fastify + zod: 定义 schema → 手动校验
import { z } from "zod";

const createPostSchema = z.object({
  title: z.string().min(1).max(200),
  content: z.string(),
  published: z.boolean().default(false),
});

app.post("/posts", async (req) => {
  const body = createPostSchema.parse(req.body);
  // body 类型自动推导为 { title: string; content: string; published: boolean }
  return { id: 1, ...body };
});
# FastAPI + Pydantic: 声明模型 → 自动校验 + 自动文档
from pydantic import BaseModel, Field

class PostCreate(BaseModel):
    title: str = Field(min_length=1, max_length=200)
    content: str
    published: bool = False

@app.post("/posts")
async def create_post(post: PostCreate):   # 声明类型,框架自动校验
    return {"id": 1, **post.model_dump()}  # model_dump() ≈ JSON.stringify()

这才是 FastAPI 真正的杀手特性:你只需声明 Pydantic 模型,FastAPI 自动完成——① 请求体验证(非法数据返回 422 + 详细错误)② 生成 OpenAPI Schema ③ Swagger 文档交互界面(/docs)④ IDE 自动补全。

3.4 完整 CRUD 示例:Posts API

下面是一个完整对照,左边 Fastify + TypeScript,右边 FastAPI:

// ═══════════ Fastify + TypeScript ═══════════
import fastify from "fastify";
import { z } from "zod";

const app = fastify();

// --- Schema ---
const PostSchema = z.object({
  id: z.number(),
  title: z.string(),
  content: z.string(),
  published: z.boolean(),
});
const CreatePostSchema = PostSchema.omit({ id: true });

type Post = z.infer<typeof PostSchema>;

// --- 内存数据库 ---
const posts: Post[] = [];
let nextId = 1;

// --- Routes ---
app.get("/posts", async () => posts);

app.get("/posts/:id", async (req) => {
  const post = posts.find(p => p.id === Number(req.params.id));
  if (!post) return req.server.notFound().status(404);
  return post;
});

app.post("/posts", async (req) => {
  const body = CreatePostSchema.parse(req.body);
  const post: Post = { id: nextId++, ...body };
  posts.push(post);
  return post;
});

app.put("/posts/:id", async (req) => {
  const idx = posts.findIndex(p => p.id === Number(req.params.id));
  if (idx === -1) return req.server.notFound().status(404);
  const body = CreatePostSchema.parse(req.body);
  posts[idx] = { ...posts[idx], ...body };
  return posts[idx];
});

app.delete("/posts/:id", async (req) => {
  const idx = posts.findIndex(p => p.id === Number(req.params.id));
  if (idx === -1) return req.server.notFound().status(404);
  posts.splice(idx, 1);
  return { message: "deleted" };
});

app.listen({ port: 3000 });
# ═══════════ FastAPI ═══════════
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

# --- Schema ---
class PostBase(BaseModel):
    title: str
    content: str
    published: bool = False

class PostCreate(PostBase):
    pass  # 继承父模型的所有字段

class Post(PostBase):
    id: int

# --- 内存数据库 ---
posts: list[Post] = []
next_id = 1

# --- Routes ---
@app.get("/posts")
async def list_posts() -> list[Post]:
    return posts

@app.get("/posts/{post_id}")
async def get_post(post_id: int) -> Post:
    post = next((p for p in posts if p.id == post_id), None)
    if not post:
        raise HTTPException(status_code=404, detail="Post not found")
    return post

@app.post("/posts", status_code=201)
async def create_post(data: PostCreate) -> Post:
    global next_id
    post = Post(id=next_id, **data.model_dump())
    next_id += 1
    posts.append(post)
    return post

@app.put("/posts/{post_id}")
async def update_post(post_id: int, data: PostCreate) -> Post:
    for i, p in enumerate(posts):
        if p.id == post_id:
            posts[i] = Post(id=post_id, **data.model_dump())
            return posts[i]
    raise HTTPException(status_code=404, detail="Post not found")

@app.delete("/posts/{post_id}")
async def delete_post(post_id: int):
    for i, p in enumerate(posts):
        if p.id == post_id:
            posts.pop(i)
            return {"message": "deleted"}
    raise HTTPException(status_code=404, detail="Post not found")

3.5 启动与热重载

操作 Node Python
启动服务 node src/index.ts uvicorn app.main:app
热重载 nodemon / tsx --watch uvicorn app.main:app --reload
生产启动 node dist/index.js uvicorn app.main:app --workers 4
# 开发模式:热重载 + 访问 /docs 交互测试
$ uvicorn app.main:app --reload
# 打开 http://localhost:8000/docs → 自动生成的 Swagger UI

3.6 这一节你该记住的

  1. FastAPI 的装饰器语法跟 Express/Fastify 几乎是 1:1 的——@app.get()@app.post(),差异只在路径参数语法({id} vs :id
  2. 参数来源由类型注解自动推断——路径参数、查询参数、请求头、Cookie 全由声明位置决定,不用手动从 req.xxx 里取
  3. Pydantic 模型 = zod schema + TypeScript 类型——声明一次,校验 + 文档 + 类型推导全有了
  4. 404 处理用 HTTPException 抛出,不是 res.status(404).send()——Python 的异常机制替代了 Express 的 response 链式调用

四、异步编程深度对比:事件循环 vs asyncio

这是 Node 开发者在 Python 世界里最容易被反直觉击中的一章。Node 的异步是”渗透式”的——你甚至不需要知道它在异步运行,一切默认就是非阻塞的。Python 则截然相反:同步是默认,异步是显式选择。理解这一层的差异,才能避免写出”看起来对、跑起来卡”的代码。

4.1 核心哲学差异:Implicit vs Explicit

维度 Node.js Python
异步模型 隐式——一切 I/O 默认非阻塞 显式——同步和异步有严格边界
事件循环 libuv,启动即运行,不可见 asyncio event loop,需显式启动
函数染色 不存在——所有函数可被 await 存在——async defdef 之间不可混用
await 范围 任何地方(ESM 顶层也可) 只能在 async def 函数内
阻塞后果 几乎不可能(除非 JSON.parse 大文件) 极易发生——一个忘记 await 就阻塞整个线程

⚠️ 核心避坑:Python 有”红蓝函数”问题。红色函数(async def)只能被红色函数 await,蓝色函数(def)不能调用红色函数。你没法像 Node 那样在任意地方 await。如果你在同步函数中调用了一个异步库的方法,你会发现根本没法拿到结果——会得到 coroutine 对象而不是返回值。

// Node: 异步是渗透式的,没有边界感
async function fetchUser(id) {
  return await db.query("SELECT * FROM users WHERE id = ?", [id]);
}

// 可以在任何地方 await,包括 ESM 顶层
const user = await fetchUser(1);
console.log(user);  // ✅ 直接工作
# Python: 异步有严格的边界
import asyncio

async def fetch_user(id: int):
    ...  # 异步数据库查询
    return {"id": id, "name": "Alice"}

# ❌ 顶层直接 await 不行(Python 3.11+ Jupyter 等交互环境除外)
# user = await fetch_user(1)  # SyntaxError!

# ✅ 必须通过 asyncio.run() 启动事件循环
user = asyncio.run(fetch_user(1))
print(user)

4.2 事件循环对比

概念 Node.js Python
事件循环实现 libuv(C 实现,多平台) asyncio(纯 Python + 少量 C)
启动方式 自动——任何 async 操作触发 手动——asyncio.run()
Task 创建 Promise 即微任务 asyncio.create_task() 创建 Task
并发等待 Promise.all([...]) asyncio.gather(*tasks)
竞速 Promise.race([...]) asyncio.wait(tasks, return_when=FIRST_COMPLETED)
全都完成 or 失败 Promise.allSettled([...]) asyncio.gather(return_exceptions=True)asyncio.TaskGroup
// Node: 并发模式
const [user, posts] = await Promise.all([
  fetchUser(1),
  fetchPosts(1),
]);

// 竞速:取第一个完成的
const first = await Promise.race([
  fetchFromSource("a"),
  fetchFromSource("b"),
]);
# Python: 并发模式
user, posts = await asyncio.gather(
    fetch_user(1),
    fetch_posts(1),
)

# 竞速:取第一个完成的
done, pending = await asyncio.wait(
    {asyncio.create_task(fetch_from_source("a")),
     asyncio.create_task(fetch_from_source("b"))},
    return_when=asyncio.FIRST_COMPLETED,
)
first = done.pop().result()
for task in pending:  # 记得取消未完成的任务
    task.cancel()

4.3 并发控制:Semaphore ≈ p-limit

Node 里你用 p-limit 控制并发数,Python 用 asyncio.Semaphore

// Node: p-limit 控制并发
import pLimit from "p-limit";

const limit = pLimit(5);
const results = await Promise.all(
  urls.map(url => limit(() => fetch(url)))
);
# Python: asyncio.Semaphore 控制并发
import asyncio

async def fetch_with_limit(url: str, sem: asyncio.Semaphore):
    async with sem:
        return await fetch_url(url)

sem = asyncio.Semaphore(5)
results = await asyncio.gather(
    *(fetch_with_limit(url, sem) for url in urls)
)

4.4 同步阻塞是异步的敌人

这是 Node 开发者迁移到 Python 后最容易犯的错——把同步阻塞函数放在 async 函数里。

# ❌ 致命错误:在 async 函数里调用同步阻塞 I/O
import time

async def fetch_all(urls):
    for url in urls:
        data = fetch_url_sync(url)    # 同步 I/O!阻塞整个事件循环
        time.sleep(1)                 # 阻塞 sleep!
        # process data...

在 Node 里没有这个问题,因为 fs.readFileSync 阻塞的是整个进程——你也知道别这么做。但 Python 里你写的函数默认就是同步的,很多库(如 requests)也是同步的。如果不加注意塞进 async 函数里,整个事件循环会被卡住。

# ✅ 正确:同步阻塞代码丢进线程池
import asyncio
import requests  # 同步 HTTP 库

async def fetch_all(urls):
    loop = asyncio.get_running_loop()

    async def fetch_one(url):
        # 把同步的 requests.get 丢进线程池执行
        resp = await loop.run_in_executor(
            None, lambda: requests.get(url)
        )
        return resp.json()

    return await asyncio.gather(*(fetch_one(u) for u in urls))
场景 Node Python
异步 HTTP 客户端 fetch() / undici httpx / aiohttp
同步 HTTP 客户端 sync-fetch(不推荐) requests(跑在线程池)
异步文件读取 fs.promises.readFile() aiofiles.open()
CPU 密集任务 Worker Threads loop.run_in_executor() / ProcessPoolExecutor
sleep 无同步 sleep(阻塞) time.sleep() 同步阻塞 vs asyncio.sleep() 异步非阻塞

⚠️ 避坑警告time.sleep()asyncio.sleep() 是两个完全不同的东西。前者阻塞当前线程(包括事件循环),后者让出控制权给其他协程。永远不要在 async 函数里用 time.sleep()

4.5 asyncio.run() 只能调一次

# ❌ 错误:多次调用 asyncio.run()
async def main():
    asyncio.run(fetch_user(1))  # RuntimeError: asyncio.run() cannot be called
                                # from a running event loop

asyncio.run() 等价于创建一个全新的事件循环并运行它。如果已有事件循环在跑(比如你在 uvicorn 里),就不能再调。框架环境(FastAPI、pytest-asyncio)里永远直接用 await

4.6 async/await 语法小结

// Node
async function fn() { ... }       // 声明
await promise;                     // 等待
async () => { ... }               // 箭头函数
for await (const x of iterable)   // 异步迭代
# Python
async def fn(): ...               # 声明
await coroutine                    # 等待
# 没有箭头函数,用普通函数或 lambda(lambda 不能是 async)
async for x in async_iterable:    # 异步迭代
    ...
async with ctx_mgr as val:        # 异步上下文管理器
    ...

Python 多了一个 async with(异步上下文管理器),比如数据库会话、信号量等场景经常用到。

4.7 这一节你该记住的

  1. Node 异步是隐式的,Python 异步是显式的——async defdef 之间有严格边界,不能混用
  2. asyncio.run() 是入口,一次启动一次退出——框架内直接用 await
  3. asyncio.gather() = Promise.all()asyncio.Semaphore = p-limit
  4. 同步阻塞函数是异步的毒药——requests.get() 塞进 async 函数会卡死整个事件循环,用 httpxrun_in_executor()
  5. time.sleep()asyncio.sleep()——在 async 函数里用前者,所有并发全被阻塞

五、Pydantic 与类型系统:Python 版 TypeScript

如果你从 TypeScript 转过来,你可能会下意识认为 Python 的类型注解跟 TS 是一回事。不是。 TypeScript 的类型只在编译时存在,运行时完全消失;Python 的类型注解默认也只是”注释”,不做任何检查。Pydantic 填补了这个空白——它把 Python 的类型注解变成了运行时的数据验证引擎

5.1 类型基础对照

类型 TypeScript Python
字符串 string str
数字 number int / float
布尔 boolean bool
数组 string[] / Array<string> list[str](3.9+)/ List[str]
元组 [string, number] tuple[str, int]
字典/对象 Record<string, number> / { [k: string]: number } dict[str, int]
可选 string | null / string | undefined str | None(3.10+)/ Optional[str]
联合类型 string | number str | int(3.10+)/ Union[str, int]
任意类型 any / unknown Any
字面量 "admin" | "user" Literal["admin", "user"]
函数类型 (x: number) => string Callable[[int], str]
泛型 Array<T> list[T]
// TypeScript: 定义一个用户类型
interface User {
  id: number;
  name: string;
  email: string;
  role: "admin" | "user";
  tags: string[];
  metadata: Record<string, unknown>;
}
# Python: 定义同样的类型
from typing import Literal
from pydantic import BaseModel
from uuid import UUID

class User(BaseModel):
    id: int
    name: str
    email: str
    role: Literal["admin", "user"]
    tags: list[str]
    metadata: dict[str, object]  # object ≈ unknown

5.2 核心差异:编译时 vs 运行时

这是理解 TypeScript 和 Pydantic 根本不同之处的关键:

维度 TypeScript Python + Pydantic
类型存在时机 仅编译时,运行时完全擦除 运行时存在,Pydantic 读取并验证
不匹配后果 编译报错(阻止构建) 运行时抛出 ValidationError(422 响应)
数据来源 开发者写的代码 外部输入(HTTP 请求、环境变量、配置文件)
类型守卫 typeofinstanceof、类型谓词 Pydantic 自动验证
默认行为 不验证,信任开发者 不信任输入,必须验证

一句话总结:TypeScript 保护你写出类型安全的代码;Pydantic 保护你的程序不被外部数据搞崩。前端的类型系统防的是你自己,后端的类型系统防的是外部世界

// TypeScript: 编译时安全,但运行时不管
const body: unknown = JSON.parse(requestBody);
// 如果 body 结构不对,下面这行在运行时可能炸
const user = body as User;  // as 断言在运行时什么都不做
user.email.toLowerCase();   // 💥 如果 email 字段不存在
# Pydantic: 运行时安全,不合规就抛异常
from pydantic import BaseModel, ValidationError

class User(BaseModel):
    id: int
    email: str

try:
    user = User(**{"id": "not-a-number", "email": None})
except ValidationError as e:
    print(e.errors())
    # [
    #   {'loc': ('id',), 'msg': 'Input should be a valid integer', ...},
    #   {'loc': ('email',), 'msg': 'Input should be a valid string', ...},
    # ]

5.3 TypeScript 类型体操 → Pydantic 等价写法

你在 TypeScript 里常用的类型操作,Pydantic 都有对应物:

TypeScript Pydantic
Partial<User> 把所有字段标 Optional,或新模型全设默认值
Omit<User, "id"> 定义 UserCreate 继承 UserBase(不含 id)
Pick<User, "name" | "email"> 继承后只保留需要的字段
z.union([A, B]) Union[A, B] / A | B
z.discriminatedUnion("type", [...]) Literal + Union
Record<string, number> dict[str, int]
Readonly<T> Pydantic 默认不可变(使用 model_copy 修改)
enum Enum / IntEnum
// TypeScript: 常见的 CRUD 模型模式
interface PostBase {
  title: string;
  content: string;
  published: boolean;
}
interface PostCreate extends PostBase {}           // 创建时无 id
interface Post extends PostBase { id: number; }     // 查询时带 id
interface PostUpdate extends Partial<PostBase> {}   // 更新时全可选
# Python/Pydantic: 同样的 CRUD 模型模式
from pydantic import BaseModel
from typing import Optional

class PostBase(BaseModel):
    title: str
    content: str
    published: bool = False

class PostCreate(PostBase):
    pass  # 创建时无 id

class Post(PostBase):
    id: int  # 查询时带 id

class PostUpdate(BaseModel):
    title: Optional[str] = None       # 更新时全可选
    content: Optional[str] = None
    published: Optional[bool] = None

5.4 Pydantic v2 核心特性

字段校验器@field_validator)——相当于 zod 的 .refine() / .transform()

// TypeScript + zod: 字段级校验和转换
const schema = z.object({
  email: z.string().email().toLowerCase(),
  age: z.number().min(0).max(150),
});
# Pydantic: 字段级校验和转换
from pydantic import BaseModel, field_validator

class UserCreate(BaseModel):
    email: str
    age: int

    @field_validator("email")
    @classmethod
    def normalize_email(cls, v: str) -> str:
        return v.strip().lower()

    @field_validator("age")
    @classmethod
    def check_age(cls, v: int) -> int:
        if v < 0 or v > 150:
            raise ValueError("Age must be between 0 and 150")
        return v

模型级校验器@model_validator)——跨字段校验:

// TypeScript + zod: 跨字段校验
const schema = z.object({
  password: z.string(),
  confirm_password: z.string(),
}).refine(data => data.password === data.confirm_password, {
  message: "Passwords don't match",
  path: ["confirm_password"],
});
# Pydantic: 跨字段校验
from pydantic import BaseModel, model_validator

class RegisterForm(BaseModel):
    password: str
    confirm_password: str

    @model_validator(mode="after")
    def check_passwords_match(self):
        if self.password != self.confirm_password:
            raise ValueError("Passwords don't match")
        return self

JSON / camelCase 自动转换

TypeScript 惯例 Python 惯例 Pydantic 配置
firstName (camelCase) first_name (snake_case) model_config = {"alias_generator": to_camel}
JSON 字段名 Python 变量名 alias + populate_by_name=True
// TypeScript: JSON 字段就是 camelCase
// API 返回 { "firstName": "Alice" }
// 直接用 user.firstName 访问
# Pydantic: JSON camelCase → Python snake_case 自动映射
from pydantic import BaseModel
from pydantic.alias_generators import to_camel

class User(BaseModel):
    first_name: str
    last_name: str

    model_config = {
        "alias_generator": to_camel,      # 导出/导入自动转 camelCase
        "populate_by_name": True,          # 两种名字都接受
    }

# 输入:{"firstName": "Alice", "lastName": "Smith"}
# 访问:user.first_name  # "Alice"
# 导出:user.model_dump(by_alias=True)  # {"firstName": "Alice", ...}

5.5 依赖注入:Depends() —— JS 世界没有的范式

FastAPI 的 Depends() 是 Node 生态里没有直接对应物的特性。它比 Express 中间件更灵活——可以注入到特定路由而不是全局应用。

概念 Express / Fastify FastAPI
认证守卫 中间件 / preHandler Depends(get_current_user)
数据库连接 中间件挂载到 req Depends(get_db)
权限校验 中间件或手动判断 Depends(require_role("admin"))
请求级缓存 手动实现 Depends() 在同一请求中复用结果
// Express: 认证中间件 → 挂载到 req,路由里手动取
app.use((req, res, next) => {
  const token = req.headers.authorization;
  req.user = decodeToken(token);  // 挂载到 req 对象
  next();
});

app.get("/me", (req, res) => {
  res.json(req.user);  // 手动从 req 取
});
# FastAPI: Depends() → 声明式注入,路由签名直接声明依赖
from fastapi import FastAPI, Depends, Header
from typing import Annotated

app = FastAPI()

async def get_current_user(authorization: str = Header(...)):
    # 从 Header 取 token → 验证 → 返回 user
    return decode_token(authorization)

@app.get("/me")
async def me(user = Depends(get_current_user)):
    return user  # user 直接可用,类型自动推导
# 进阶:链式依赖 + 可复用
async def get_db():
    db = Database()
    try:
        yield db
    finally:
        await db.close()

async def get_current_user(
    token: str = Depends(get_token),
    db = Depends(get_db),
):
    return await db.users.find_by_token(token)

# 路由里只需声明最终需要的依赖
@app.get("/me")
async def me(user = Depends(get_current_user)):
    return user

理解 Depends():它跟 Express 中间件的区别在于——中间件是管道(请求流过),Depends 是注入(路由声明需要什么)。前者隐式,后者显式,显式依赖让测试和复用更简单。

5.6 这一节你该记住的

  1. TypeScript 类型止于编译时,Pydantic 类型活在运行时——前者防你写错,后者防外部数据搞崩程序
  2. Pydantic 模型定义一次,校验 + 序列化 + 文档全搞定——跟 zod + TS 的体验一致,但多了运行时保障
  3. alias_generator 解决 camelCase ↔ snake_case 问题——一行配置搞定前后端命名风格差异
  4. Depends() 是 Express 中间件的升级版——声明式、可组合、可复用、按需注入

六、数据库与 ORM:从 Prisma/Drizzle 到 SQLAlchemy

Python 的 ORM 生态跟 Node 一样是分层级的。SQLAlchemy 是 Python 生态中无可争议的王者,但同时提供了两层 API——高级 ORM 和低级 Core——让你可以像用 Prisma 一样全 ORM,也可以像用 Drizzle/Knex 一样更接近 SQL。

6.1 ORM 生态分层对照

层级 Node.js Python
全功能 ORM Prisma SQLAlchemy ORM
薄层查询构建器 Drizzle / Kysely SQLAlchemy Core
原始 SQL 执行 pg / mysql2 psycopg2 / aiomysql / SQLAlchemy Core text()
迁移工具 Prisma Migrate / Drizzle Kit Alembic(SQLAlchemy 官方)
异步支持 原生(Prisma 自带) SQLAlchemy 2.0+ create_async_engine

选型建议:如果你习惯 Prisma 的体验,用 SQLAlchemy ORM;如果你更偏向 Drizzle/Knex 的手写查询风格,用 SQLAlchemy Core。两者可以混用——同一个项目里 ORM 和 Core 共享同一个 engine 和连接池。

6.2 模型定义:Prisma Schema → SQLAlchemy ORM

// Prisma: 声明式 Schema
model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  posts     Post[]
  createdAt DateTime @default(now())
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  authorId  Int
  author    User     @relation(fields: [authorId], references: [id])
  createdAt DateTime @default(now())
}
# SQLAlchemy ORM: 声明式模型(2.0 风格)
from sqlalchemy import String, Boolean, DateTime, ForeignKey, func
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from datetime import datetime

class Base(DeclarativeBase):
    pass

class User(Base):
    __tablename__ = "users"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    email: Mapped[str] = mapped_column(String(255), unique=True)
    name: Mapped[str | None] = mapped_column(String(100), nullable=True)
    posts: Mapped[list["Post"]] = relationship(back_populates="author")
    created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())

class Post(Base):
    __tablename__ = "posts"

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    title: Mapped[str] = mapped_column(String(200))
    content: Mapped[str | None] = mapped_column(nullable=True)
    published: Mapped[bool] = mapped_column(Boolean, default=False)
    author_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
    author: Mapped["User"] = relationship(back_populates="posts")
    created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())

6.3 数据库连接与 Session 管理

这是 Prisma 和 SQLAlchemy 差异最大的地方。Prisma 为你管理连接池和事务生命周期——你引入 PrismaClient 就开始工作。SQLAlchemy 需要显式管理 Engine + Session

概念 Prisma SQLAlchemy
连接池 PrismaClient 自动管理 create_engine() + pool_size 参数
工作单元 天然事务(每次查询即事务) Session() 显式管理
请求级会话 不需要特别处理 Depends(get_db) + yield pattern
异步引擎 PrismaClient 默认异步 create_async_engine() + AsyncSession
// Prisma: 引入即用,连接池自动管理
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

const users = await prisma.user.findMany({
  include: { posts: true },
});
# SQLAlchemy 2.0 async: 显式 Engine + AsyncSession
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker

engine = create_async_engine(
    "postgresql+asyncpg://user:pass@localhost/mydb",
    pool_size=10,        # 连接池大小
    echo=False,          # 设为 True 打印所有 SQL(调试用)
)

AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)

async def get_db():
    async with AsyncSessionLocal() as session:
        yield session
# FastAPI 集成:Depends + yield 保证请求结束后关闭 session
from fastapi import FastAPI, Depends

app = FastAPI()

@app.get("/users")
async def list_users(db: AsyncSession = Depends(get_db)):
    result = await db.execute(
        select(User).options(selectinload(User.posts))
    )
    return result.scalars().all()

⚠️ 避坑警告session.expire_on_commit 默认是 True,这意味着 commit() 后所有已加载的实例属性会被清空,下次访问会触发 lazy load。在异步环境或 FastAPI 的 Pydantic 序列化场景下会意外触发额外 SQL,甚至因 session 已关闭而报错。创建 async_sessionmaker 时务必设 expire_on_commit=False

6.4 查询语法对照

操作 Prisma SQLAlchemy
查全部 findMany() select(Model)
查一条 findUnique({ where: { id } }) session.get(Model, id)
条件过滤 where: { published: true } .where(Model.published == True)
关联加载 include: { posts: true } .options(selectinload(User.posts))
分页 skip + take .offset().limit()
排序 orderBy: { createdAt: "desc" } .order_by(Model.created_at.desc())
创建 create({ data: {...} }) session.add(instance) + commit()
更新 update({ where, data }) 修改属性 + commit()
删除 delete({ where }) session.delete(instance) + commit()
聚合 _count() / groupBy func.count() / .group_by()
原生 SQL $queryRaw session.execute(text(sql))
// Prisma: 复杂查询
const posts = await prisma.post.findMany({
  where: {
    published: true,
    author: { email: { contains: "@gmail.com" } },
  },
  include: { author: true },
  orderBy: { createdAt: "desc" },
  skip: 0,
  take: 10,
});
# SQLAlchemy: 等价复杂查询
from sqlalchemy import select
from sqlalchemy.orm import selectinload

stmt = (
    select(Post)
    .options(selectinload(Post.author))  # 等同于 Prisma 的 include
    .where(
        Post.published == True,
        User.email.contains("@gmail.com"),
    )
    .join(Post.author)                   # where 用到了关联表,需要显式 join
    .order_by(Post.created_at.desc())
    .offset(0)
    .limit(10)
)

result = await db.execute(stmt)
posts = result.scalars().all()

关键差异:Prisma 的 include 可以根据 where 条件自动推断需要的 join;SQLAlchemy 需要显式写 .join()。但这也给了你更精确的控制——Prisma 的自动 join 有时候会生成你不想看到的 SQL。

6.5 关联加载策略:Prisma include → SQLAlchemy loader

Prisma 的 include 总是发一条带 JOIN 的查询(或 Prisma 优化后的批量查询)。SQLAlchemy 给了你更多选择:

策略 说明 SQL 适用场景
selectinload() 第二条 SELECT IN 查询 SELECT ... WHERE id IN (1,2,3) 一对多/多对多,默认首选
joinedload() LEFT JOIN 一次查出 LEFT JOIN ... ON ... 一对一/多对一
lazyload 懒加载(默认) 访问属性时再查一次 避免使用——N+1 之源
# ✅ 推荐:显式声明加载策略,避免 N+1
stmt = select(User).options(
    selectinload(User.posts),          # 一对多:第二条 SELECT IN
    joinedload(User.profile),          # 一对一:JOIN
)

6.6 数据库迁移:Prisma Migrate → Alembic

操作 Prisma Alembic
初始化 npx prisma init alembic init alembic
生成迁移 npx prisma migrate dev --name xxx alembic revision --autogenerate -m "xxx"
执行迁移 npx prisma migrate deploy alembic upgrade head
回滚 npx prisma migrate reset alembic downgrade -1
查看历史 npx prisma migrate status alembic history
生成 SQL Prisma Migrate 自动 alembic upgrade head --sql
# Node
$ npx prisma migrate dev --name add_user_table
# → prisma/migrations/20250101000000_add_user_table/migration.sql

# Python
$ alembic revision --autogenerate -m "add_user_table"
# → alembic/versions/abc123_add_user_table.py
$ alembic upgrade head

⚠️ 避坑警告:Alembic 的 --autogenerate 不是万能的。表重命名、列类型变更(如 String(100)String(200))不会自动检测到。生成后务必人工检查生成的迁移文件。

6.7 这一节你该记住的

  1. Prisma → SQLAlchemy ORM,Drizzle → SQLAlchemy Core——分层结构一致,可以混用
  2. Session 管理是核心差异——Prisma 自动,SQLAlchemy 显式。用 Depends(get_db) + async_sessionmaker 模式封装
  3. expire_on_commit=False 是必须配置——否则 Pydantic 序列化时各种幽灵错误
  4. selectinload() = Prisma 的 include——默认用它做关联加载,避免 N+1
  5. Alembic autogenerate 不是银弹——生成后人工检查,表重命名不会自动识别

七、中间件与请求生命周期

Express/Koa 的洋葱模型中间件是 Node 开发者最熟悉的概念之一。FastAPI 的中间件体系基于 Starlette,模型略有不同——理解这个差异,才能在两种生态间自由切换。

7.1 中间件模型对比

维度 Express / Koa FastAPI / Starlette
执行模型 洋葱模型(next() 前后执行) 洋葱模型(call_next() 前后执行)
注册方式 app.use(fn) app.add_middleware(Cls)
中间件形式 函数 类(BaseHTTPMiddleware)或纯 ASGI 中间件
修改请求 req.body = ... / 挂载属性 修改 request.scope / 注入到 request.state
修改响应 res.json(...) 修改 response 对象并返回
路由级中间件 router.use(fn) APIRouter(dependencies=[Depends()])
// Express: 洋葱模型——中间件是函数
app.use(async (req, res, next) => {
  const start = Date.now();
  await next();                      // 执行后续中间件和路由
  const ms = Date.now() - start;
  res.set("X-Response-Time", `${ms}ms`);
});
# FastAPI: 洋葱模型——中间件是类或函数
from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware
import time

app = FastAPI()

class TimingMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        start = time.time()
        response = await call_next(request)   # 执行后续中间件和路由
        elapsed = time.time() - start
        response.headers["X-Response-Time"] = str(elapsed)
        return response

app.add_middleware(TimingMiddleware)

7.2 常用中间件对照

功能 Express / Fastify FastAPI
CORS cors() CORSMiddleware
安全头 helmet() 无内置等价物,需手动配置或使用 Secure 等第三方
请求日志 morgan logging 模块 + 自定义中间件
限流 express-rate-limit slowapi
Body 解析 express.json() / express.urlencoded() 内置(与框架一体)
静态文件 express.static("public") StaticFiles
压缩 compression() GZipMiddleware
可信代理 trust proxy 设置 TrustedHostMiddleware
// Express: 常用中间件组合
const express = require("express");
const cors = require("cors");
const helmet = require("helmet");
const morgan = require("morgan");

const app = express();

app.use(helmet());
app.use(cors({ origin: "https://example.com" }));
app.use(morgan("combined"));
app.use(express.json());
# FastAPI: 常用中间件组合
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://example.com"],
    allow_methods=["*"],
    allow_headers=["*"],
)
app.add_middleware(GZipMiddleware)
# 请求日志 → 自定义中间件(见下方 7.3)

⚠️ 避坑警告add_middleware 的顺序就是执行顺序——最后注册的最先执行(Starlette 的实现细节)。如果你先加 CORS 再加 日志,日志会比 CORS 先执行。这和 Express 的 app.use() 顺序语义恰好相反。

7.3 自定义中间件示例

// Express: 简单中间件——请求日志 + 用户注入
app.use(async (req, res, next) => {
  console.log(`${req.method} ${req.url}`);

  const token = req.headers.authorization;
  if (token) {
    req.user = await decodeToken(token);
  }
  next();
});
# FastAPI: 等价的中间件 + Depends 模式
# 方案一:纯中间件(对全局生效)
class LoggingMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        print(f"{request.method} {request.url}")
        return await call_next(request)

app.add_middleware(LoggingMiddleware)

# 方案二:Depends(推荐——按路由注入,更精确)
from fastapi import Depends

async def get_current_user(authorization: str = Header(None)):
    if not authorization:
        return None
    return await decode_token(authorization)

@app.get("/me")
async def me(user = Depends(get_current_user)):
    return user

选型建议全局逻辑用中间件(日志、CORS、压缩),路由级逻辑用 Depends()(认证、权限、数据库连接)。后者更灵活,且不会在不需要的路由上浪费数据库连接。

7.4 请求生命周期总览

一条请求在两种框架中的完整路径:

Express 生命周期:
  Request → Middleware 1(next前) → ... → Middleware N(next前) → Route Handler
         → Middleware N(next后) → ... → Middleware 1(next后) → Response

FastAPI 生命周期:
  Request → Middleware(call_next前)
         → Exception Handlers(未被捕获的异常到此兜底)
         → Dependencies(按路由声明的 Depends 链)
         → Route Handler
         → Dependencies 清理(yield 后的 finally 块)
         → Response 序列化(Pydantic → JSON)
         → Middleware(call_next后)
         → Response
钩子 Express FastAPI
请求进入时 中间件 next() 之前 中间件 call_next() 之前
路由处理前 中间件链最后 Depends 依赖链
路由处理 Route handler Route handler
响应发出前 中间件 next() 之后 中间件 call_next() 之后
请求结束时 无显式清理 Depends yield 的 finally
异常兜底 app.use((err, req, res, next) => {}) @app.exception_handler()

7.5 这一节你该记住的

  1. FastAPI 中间件也是洋葱模型——只是注册方式从 app.use(fn) 变为 app.add_middleware(Cls)
  2. 注册顺序和执行顺序相反——最后 add_middleware 的最先执行
  3. 全局用中间件,路由级用 Depends——把认证、数据库连接从中间件里解放出来
  4. Python 没有 helmet——安全头需要手动配置,或用第三方 Secure 库

八、错误处理与日志体系

try/catch 在两种语言中都在被大量使用,但异常体系的哲学和处理模式有显著差异。Python 的异常系统比 Node 更加”融入血液”——很多你习惯用 return value 表达的东西,Python 社区直接用异常。

8.1 try/catch vs try/except

语法层面几乎一致,但几个细微差异值得注意:

特性 JavaScript Python
捕获异常 try { ... } catch (err) { ... } try: ... except Exception as err: ...
最终块 finally { ... } finally: ...
无异常时执行 无直接等价物 else: 块(仅当 try 块无异常时执行)
多异常类型 catch (err) { if (err instanceof TypeError) ... } except TypeError: ...(原生支持多个 except 分支)
重新抛出 throw err raise(在 except 块中不带参数即重新抛出当前异常)
自定义异常 class MyError extends Error {} class MyError(Exception): pass
// JS: 多类型异常——手动判断
try {
  riskyOperation();
} catch (err) {
  if (err instanceof ValidationError) {
    console.log("Validation:", err.message);
  } else if (err instanceof TimeoutError) {
    console.log("Timeout:", err.message);
  } else {
    throw err;  // 不是预期异常,继续抛
  }
}
# Python: 多类型异常——原生多分支
try:
    risky_operation()
except ValidationError as e:
    print(f"Validation: {e}")
except TimeoutError as e:
    print(f"Timeout: {e}")
else:
    print("No error occurred")   # JS 没有的 else 块
finally:
    cleanup()

⚠️ 避坑警告except 的顺序很重要!Python 从上到下匹配第一个兼容的 exception 类型。如果 except Exception 写在 except ValueError 前面,后面的分支永远不会执行——因为 ValueErrorException 的子类。

8.2 框架级异常处理

场景 Express FastAPI
抛 HTTP 异常 throw createHttpError(404, "Not found") raise HTTPException(status_code=404, detail="Not found")
全局异常处理器 app.use((err, req, res, next) => {...}) @app.exception_handler(SomeError)
未处理异常 Express 默认返回 HTML FastAPI 返回 JSON {"detail": "Internal Server Error"}
请求体验证失败 手动处理 Pydantic 自动返回 ValidationError → 422 JSON
// Express: 全局异常处理
app.use((err, req, res, next) => {
  console.error(err.stack);
  const status = err.statusCode || 500;
  res.status(status).json({
    error: { message: err.message, status },
  });
});
# FastAPI: 全局异常处理——装饰器风格
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse

app = FastAPI()

@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
    return JSONResponse(
        status_code=exc.status_code,
        content={"error": {"message": exc.detail, "status": exc.status_code}},
    )

@app.exception_handler(Exception)
async def unhandled_exception_handler(request: Request, exc: Exception):
    # 在生产环境中你可能会记录到 Sentry/Datadog
    print(f"Unhandled error: {exc}")
    return JSONResponse(
        status_code=500,
        content={"error": {"message": "Internal Server Error", "status": 500}},
    )

8.3 日志体系:winston/pino → logging/loguru

这是 Node 和 Python 差异最大的基础设施之一。Node 的日志全是第三方(winston、pino、bunyan);Python 的 logging标准库内置的四级日志系统,理念接近 Java 的 log4j。

概念 JS/TS (winston/pino) Python (logging) Python (loguru,推荐)
获取 Logger winston.createLogger() logging.getLogger(__name__) from loguru import logger(全局单例)
日志级别 error/warn/info/debug ERROR/WARNING/INFO/DEBUG 相同 + 自定义级别
格式化 winston.format.combine(...) logging.Formatter(fmt, datefmt) logger.add(sink, format=...)
结构化日志 pino() 默认 JSON python-json-logger logger.add(sink, serialize=True)
请求日志 morgan 自定义中间件 自定义中间件
文件轮转 winston-daily-rotate-file logging.handlers.RotatingFileHandler logger.add("file.log", rotation="1 day")
// Node + pino: 结构化日志
import pino from "pino";

const logger = pino({
  level: "info",
  transport: { target: "pino-pretty" },
});

logger.info({ userId: 1, action: "login" }, "User logged in");
# Python + loguru: 结构化日志——一行初始化
from loguru import logger

logger.add("app.log", rotation="1 day", retention="7 days", serialize=True)
logger.add("app_error.log", level="ERROR")  # 错误单独记一个文件

logger.info("User logged in", user_id=1, action="login")
# loguru 的上下文参数自动序列化为 JSON

选型建议:如果你习惯 pino 的体验,直接用 Loguru。它比标准库 logging 简洁太多——无需配置 dict、无需 Handler 和 Formatter 的黑话,一行 logger.add() 搞定文件轮转和格式化。

8.4 请求日志中间件

// Express + morgan: 请求日志
app.use(morgan(':method :url :status :response-time ms'));
# FastAPI: 自定义请求日志
import time
from starlette.middleware.base import BaseHTTPMiddleware

class RequestLoggingMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        start = time.time()
        response = await call_next(request)
        elapsed = (time.time() - start) * 1000
        logger.info(
            f"{request.method} {request.url.path}",
            status=response.status_code,
            duration_ms=round(elapsed, 2),
        )
        return response

app.add_middleware(RequestLoggingMiddleware)

8.5 这一节你该记住的

  1. try/catchtry/except——语法几乎一样,但 Python 多了 else: 和多重 except 分支
  2. except 顺序是关键的——先捕获子类再捕获父类,否则后面的分支永远不会执行
  3. 异常是 Python 的”一等公民”——很多你在 JS 里用 return value 检查的场景,Python 直接用异常
  4. 日志用 loguru,别折腾标准库 logging——一行的 logger.add() 代替几十行 logging dictConfig

九、测试:从 Jest/Vitest 到 pytest

如果你习惯了 Jest 或 Vitest 的体验,pytest 会让你有回家的感觉——它是 Python 生态中最受爱戴的测试框架,设计哲学跟 Jest 意外地接近。但有一个特性是 Jest 不具备的:fixture 依赖注入体系,这可能是 pytest 最强大的武器。

9.1 基础对比

特性 Jest / Vitest pytest
测试文件命名 *.test.ts / *.spec.ts test_*.py / *_test.py
测试函数命名 test("description", fn) def test_description():
测试分组 describe("group", fn) class TestGroup:
断言 expect(x).toBe(y) assert x == y
异步测试 async () => { await ... } async def ... + pytest-asyncio
setup/teardown beforeEach / afterEach fixture(依赖注入,更强大)
Mock jest.fn() / jest.mock() unittest.mock.Mock / .patch()
参数化测试 it.each([...]) @pytest.mark.parametrize
覆盖率 --coverage (内置) pytest-cov 插件
运行单个文件 npx jest path/to/test.ts pytest path/to/test.py
watch 模式 --watch ptw / pytest-watch 插件
// Jest: 一个完整的测试文件
import { describe, it, expect, beforeEach } from "vitest";

describe("Calculator", () => {
  let calc;

  beforeEach(() => {
    calc = new Calculator();
  });

  it("should add two numbers", () => {
    expect(calc.add(1, 2)).toBe(3);
  });

  it.each([
    [1, 2, 3],
    [0, 0, 0],
    [-1, 1, 0],
  ])("add(%i, %i) = %i", (a, b, expected) => {
    expect(calc.add(a, b)).toBe(expected);
  });
});
# pytest: 等价的测试文件
import pytest

class TestCalculator:
    @pytest.fixture
    def calc(self):
        return Calculator()     # 注入到测试方法

    def test_add(self, calc):
        assert calc.add(1, 2) == 3

    @pytest.mark.parametrize("a,b,expected", [
        (1, 2, 3),
        (0, 0, 0),
        (-1, 1, 0),
    ])
    def test_add_parametrized(self, calc, a, b, expected):
        assert calc.add(a, b) == expected

9.2 Fixture:pytest 的杀手级特性

beforeEachafterEach 是你知道的模式。pytest 的 fixture 让这个概念进化了:

特性 Jest pytest fixture
注入方式 beforeEach 中赋值给外部变量 直接作为测试函数参数接收
作用域 function(每次) function / class / module / session
复用方式 共享 beforeEach 代码 fixture 自动发现 + 依赖注入
清理 afterEach / afterAll yield 之后的代码(和 Depends 模式一致)
依赖链 手动控制 beforeEach 顺序 fixture 可以依赖其他 fixture
// Jest: beforeEach 手动管理状态
let db, user;

beforeEach(async () => {
  db = await createTestDB();          // 创建数据库
  await db.migrate();
  user = await db.createUser({        // 创建用户
    email: "test@example.com",
  });
});

afterEach(async () => {
  await db.close();                    // 清理
});

it("fetches user", async () => {
  const result = await getUser(db, user.id);
  expect(result.email).toBe("test@example.com");
});
# pytest: fixture 链式依赖 + yield 清理
import pytest

@pytest.fixture
async def db():
    db = await create_test_db()
    await db.migrate()
    yield db                  # 测试用这个值
    await db.close()          # 测试结束后自动清理

@pytest.fixture
async def user(db):           # user fixture 依赖 db fixture
    return await db.create_user(email="test@example.com")

async def test_get_user(db, user):     # 需要啥声明啥
    result = await get_user(db, user.id)
    assert result.email == "test@example.com"

理解 fixture:如果你只看到一个 declare-test-use 的模式,你只看到了表层。真正强大的是 fixture 的可组合性——user 依赖 dbauth_user 依赖 user。每个测试函数只声明它需要的最终结果,pytest 自动构建完整的依赖图。

9.3 Mock 对比

操作 Jest Python unittest.mock
创建 mock 函数 const fn = jest.fn() fn = Mock()
返回值 fn.mockReturnValue(42) fn.return_value = 42
副作用 fn.mockImplementation(x => x*2) fn.side_effect = lambda x: x * 2
验证调用 expect(fn).toHaveBeenCalled() fn.assert_called()
验证参数 expect(fn).toHaveBeenCalledWith(1, 2) fn.assert_called_with(1, 2)
模块 Mock vi.mock("./module")jest.mock() patch("path.to.module.Class")
重置 fn.mockClear() fn.reset_mock()
// Jest: Mock HTTP 请求
import { vi } from "vitest";

vi.mock("./api", () => ({
  fetchUser: vi.fn().mockResolvedValue({ id: 1, name: "Alice" }),
}));
# pytest: Mock HTTP 请求
from unittest.mock import patch, AsyncMock

@patch("app.services.api.fetch_user")
async def test_my_service(mock_fetch):
    mock_fetch.return_value = {"id": 1, "name": "Alice"}
    result = await my_service.get_user(1)
    mock_fetch.assert_called_once_with(1)
    assert result.name == "Alice"

⚠️ 避坑警告patch() 的路径参数是基于模块导入路径,不是文件路径。如果你在 app/services/user.pyfrom .api import fetch_user,patch 的路径应该是 "app.services.user.fetch_user",不是 "app.services.api.fetch_user"。这个陷阱每个新手都会掉进去。

9.4 HTTP 测试:supertest → TestClient

// Node + supertest: 集成测试
import request from "supertest";

const res = await request(app)
  .post("/posts")
  .send({ title: "Hello", content: "..." })
  .expect(201);

expect(res.body.title).toBe("Hello");
# FastAPI + TestClient: 集成测试
from fastapi.testclient import TestClient

client = TestClient(app)

def test_create_post():
    response = client.post(
        "/posts",
        json={"title": "Hello", "content": "..."},
    )
    assert response.status_code == 201
    assert response.json()["title"] == "Hello"

对于异步测试,用 httpx.AsyncClient 走 ASGI transport:

import pytest
from httpx import AsyncClient, ASGITransport

@pytest.mark.asyncio
async def test_create_post_async():
    async with AsyncClient(
        transport=ASGITransport(app=app),
        base_url="http://test",
    ) as client:
        response = await client.post(
            "/posts",
            json={"title": "Hello", "content": "..."},
        )
        assert response.status_code == 201

9.5 这一节你该记住的

  1. pytest fixture 是 beforeEach 的进化版——依赖注入、可组合、自动清理,比手动管理状态强一个量级
  2. assert 就是断言,不需要 expect().toBe() 那一套
  3. patch() 的目标路径是导入路径——错误的目标路径是 pytest 最大的坑
  4. TestClient 对标 supertest——同步测试用 TestClient,异步测试用 httpx.AsyncClient + ASGI transport
  5. 别忘记 pytest-asyncio——没有它,async def 测试函数不会被执行

十、部署与生产化

最后一节聚焦于把开发完的服务推到生产环境。Node 的部署模式你已经了如指掌——pm2 start 或容器化。Python 的部署多了一层”协议”的概念,这是 Node 开发者最容易困惑的地方。

10.1 WSGI / ASGI:Python 独有的协议层

Node 没有 WSGI/ASGI 这种概念——HTTP server 和 Application 的接口是隐式的。Python 有一个标准化的服务器-应用协议。这不是你需要手写的东西,但理解它能避免选错部署方案。

协议 适用框架 类比理解
WSGI(同步) Flask, Django(同步模式) 类似 CGI 但保持进程常驻——只处理同步请求
ASGI(异步) FastAPI, Starlette, Django(异步模式) 类似 Node 的 HTTP 服务器——支持 WebSocket、长轮询、异步 I/O

一句话:FastAPI 必须用 ASGI 服务器启动(uvicorn),Flask 用 WSGI 服务器(gunicorn)。别搞混——用 gunicorn 跑 FastAPI 需要 uvicorn worker

10.2 应用服务器对照

功能 Node.js Python
开发服务器 node index.js uvicorn app.main:app --reload
生产服务器 node dist/index.js uvicorn app.main:app --workers 4
进程管理 pm2 gunicorn / supervisor / systemd
Worker 模式 pm2 start -i 4 gunicorn -w 4
# Node: 生产启动
$ pm2 start dist/index.js -i 4 --name my-app

# Python: 生产启动
$ uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4
# 或者用 gunicorn 管理 uvicorn workers(更稳定):
$ gunicorn app.main:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000

⚠️ 避坑警告python app.py 里面加 app.run() 只能在开发环境用!它启动的是单线程、非生产级别的服务器。生产环境必须用 uvicorn/gunicorn,否则并发、稳定性、优雅关闭全都没保障。

10.3 Docker 化

# Node Dockerfile
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json .
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
# Python Dockerfile (FastAPI)
FROM python:3.12-slim AS builder
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN pip install uv && uv sync --frozen --no-dev

FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /app/.venv ./.venv
COPY app/ ./app/
ENV PATH="/app/.venv/bin:$PATH"
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
# .dockerignore 注意
__pycache__
*.pyc
.venv
.idea
.git
.env
*.egg-info
# docker-compose.yml(Node 和 Python 的结构完全一致)
services:
  app:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql+asyncpg://user:pass@db:5432/mydb
    depends_on:
      - db
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: mydb

10.4 环境变量管理

操作 Node Python
读取变量 process.env.PORT os.environ["PORT"]
带默认值 process.env.PORT || 3000 os.environ.get("PORT", "3000")
.env 加载 dotenv python-dotenv
类型安全配置 zod + process.env pydantic-settings
// Node: 生产级配置管理
import { z } from "zod";

const config = z.object({
  PORT: z.coerce.number().default(3000),
  DATABASE_URL: z.string().url(),
  LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
}).parse(process.env);
# Python: 生产级配置管理
from pydantic_settings import BaseSettings

class Config(BaseSettings):
    port: int = 3000
    database_url: str
    log_level: str = "info"

    model_config = {"env_file": ".env", "env_prefix": ""}

config = Config()  # 自动读取 .env 并验证

10.5 健康检查与优雅关闭

// Node: 优雅关闭
process.on("SIGTERM", async () => {
  console.log("SIGTERM received, shutting down...");
  await server.close();
  await db.disconnect();
  process.exit(0);
});
# FastAPI: 优雅关闭
from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app: FastAPI):
    # 启动时
    await database.connect()
    yield
    # 关闭时——自动响应 SIGTERM/SIGINT
    await database.disconnect()

app = FastAPI(lifespan=lifespan)

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

uvicorn 本身处理 SIGTERM/SIGINT 信号,并使用 lifespan 上下文优雅关闭。不需要像 Node 那样手动绑定信号处理器。

10.6 性能与生产 checklist

检查项 Node Python
生产服务器 pm2 + node / 容器化 uvicorn + gunicorn / 容器化
Worker 数 CPU 核数 × 1 CPU 核数 × 1(uvicorn --workers
健康检查 /health 端点 /health 端点
优雅关闭 信号处理 + server.close() lifespan 上下文
日志 stdout → docker / datadog stdout → docker / datadog
反向代理 nginx(静态文件 + SSL 终止) nginx(静态文件 + SSL 终止)
环境变量 .env + 运行时注入 .env + 运行时注入

10.7 这一节你该记住的

  1. FastAPI 必须用 ASGI 服务器——uvicorn 是标配,gunicorn 是可选的进程管理器外壳
  2. python app.py 只用于开发——生产环境必须用 uvicorn 多 worker 模式
  3. Dockerfile 结构跟 Node 几乎一致——多阶段构建 + uv 安装依赖,注意 __pycache__ 相关文件
  4. 配置管理用 pydantic-settings——和 zod + process.env 同模式,但运行时验证
  5. lifespan 代替手动信号处理——uvicorn 自动处理 SIGTERM/SIGINT,框架的 lifespan 执行优雅关闭