选型结论先行:IO 密集的 API 服务(尤其是要扛高并发读、对接异步数据库与异步第三方调用的场景)首选 FastAPI;重内容管理、需要 Admin 与完整脚手架的团队仍应选 Django;小工具与原型 Flask 依旧够用;前后端同栈的团队 Express 依然是自然选择。本节用五个维度把这个结论的推理过程摆出来。
阅读完本节,你应当能够:
框架对比最容易变成信仰之争,原因往往是评估维度不清。我们固定五个维度,每个维度都给两边的代码事实,不谈感受:
四个框架都用装饰器或链式调用注册路由,表面相似,参数语义差别很大。
Flask 把路径参数当字符串交给你,类型转换自己写;查询参数用 request.args.get 手取。Express 用 :item_id 占位,参数在 req.params 里,同样是无类型字符串,校验靠手写或 Joi 中间件。Django REST framework 用显式的视图类加解析器,路径参数在 URL 配置里用正则或路径转换器声明类型——<int:pk> 是 Django 里少数接近声明式类型的机制。
FastAPI 的差异在前面演示过:类型写在签名里,转换与拦截自动完成。再加一个对照——同一个"分页查询"接口的参数声明:
# Flask:类型、默认值、范围校验全手写 page = request.args.get("page", 1, type=int) size = request.args.get("size", 10, type=int) if size < 1 or size > 100: return jsonify(error="size out of range"), 400
# FastAPI:签名即全部 def list_items(page: int = 1, size: int = Query(10, ge=1, le=100)): ...
注意 Flask 版的 type=int 在传入 ?page=abc 时会静默回落到默认值 1——这是一个真实的坑:用户传错参数却拿到了第一页数据,错误被吞掉了。FastAPI 版会直接返回 422 并指明 page 字段非法。静默容错与显式报错,是两种工程哲学,API 对外服务时后者几乎总是更安全。
请求体校验是差异最大的地方。Flask 拿到 JSON 后是裸 dict,字段存在性、类型、格式全靠自己 if 判断,或引入 marshmallow 另写一套 Schema。Express 生态常用 Joi 或 zod,同样是显式调用 validate。DRF 的 serializer 最接近:定义一个 Serializer 类,调 is_valid() 后取 validated_data。
FastAPI 直接把校验绑在参数上:
from pydantic import BaseModel, Field, EmailStr class CreateUser(BaseModel): name: str = Field(min_length=1, max_length=50) email: EmailStr age: int = Field(ge=0, le=150) @app.post("/users") def create_user(payload: CreateUser): ... # 进到这里时数据一定合法
业务函数从第一行起就操作合法的强类型对象。对比 DRF:serializer 能力相当,但需要单独的类、显式的 is_valid() 调用,而且 serializer 与 ORM 模型的关系(ModelSerializer、嵌套写法)有自己的学习成本。对比 Flask + marshmallow:能力也够,但两套体系(视图 + Schema)之间的粘合代码全是你写。
结论:校验能力的上限四个生态都够(都接得住复杂规则),差别在"校验与接口定义的耦合度"——FastAPI 把两者压缩到一处,漂移空间最小。
Flask 生态有 flask-smorest、apispec,能生成 OpenAPI,但需要显式装饰与维护。Express 有 swagger-jsdoc,靠注释块生成,注释与代码的同步全凭自觉。DRF 内置 OpenAPI 生成(新版本用 drf-spectacular 增强),是四者里唯一"开箱即用程度"接近 FastAPI 的。
FastAPI 的优势不在"能生成",而在生成物与运行行为的同源:文档描述的约束(类型、范围、必填)就是运行时真正执行的校验规则,因为两者读的是同一份签名。这一点 drf-spectacular 通过大量推断也能做到大部分,但边界场景(自定义校验器如何体现在 schema)仍需手动提示。
这是 FastAPI 的主场,也是误解最多的一维。先给一个可直接运行的对照实验,感受两种执行模型的差别(细节机制第 3 章拆解):
import asyncio import time from fastapi import FastAPI app = FastAPI() @app.get("/sync-wait") def sync_wait(): time.sleep(1) # 同步睡 1 秒 return {"model": "sync"} @app.get("/async-wait") async def async_wait(): await asyncio.sleep(1) # 异步让出 1 秒 return {"model": "async"}
用并发工具同时打五十个请求:两个接口都能响应,但如果把同步版放进 async 路由(去掉 def 改成 async def 且不改 sleep),事件循环被逐个卡住,五十个请求要排队近五十秒。**同样的等待时长,执行模型不同,吞吐天差地别。**这正是"async def 不等于更快"的直观教材:关键不在关键字,在于等待时是否让出控制权。
这是 FastAPI 的主场,也是误解最多的一维。
再横向看四个框架的底座差异。
Flask 直到 2.x 才支持 async 视图,但默认栈仍是 WSGI 同步模型,async 视图在缺少事件循环支撑的部署下并发收益有限,社区共识仍是"Flask 不为异步而生"。Django 自 3.1 引入 async 视图、4.x 逐步异步化 ORM,方向明确但生态(中间件、第三方包)的异步适配仍在进行中。Express 天生单线程事件循环,异步是它的母语,但 CPU 密集任务会卡死整个进程,需要 worker_threads 或拆服务。
FastAPI 原生 ASGI:async 路由跑在事件循环上,等待数据库或外部 API 时让出控制权,单进程可挂起大量并发请求。但必须强调反面:如果你在 async 路由里用了同步阻塞的数据库驱动(比如普通 psycopg2),每个请求都会卡住事件循环,吞吐比同步栈更差。异步是收益还是灾难,取决于整条调用链是否异步——这个话题第 3 章用整章展开。
给一个数量级感受(具体数值依赖场景,仅示方向):同样单进程等待外部 100ms 的 API,同步模型每秒约处理几十请求,事件循环模型可同时挂起上千个等待,吞吐提升一个数量级以上。前提是"等待 IO"而非"计算"。
| 维度 | FastAPI | Flask | Django/DRF | Express |
|---|---|---|---|---|
| 参数类型转换 | 签名自动 | 手写 | 路径转换器部分支持 | 手写或 Joi |
| 请求体校验 | Pydantic 绑定参数 | 自理或 marshmallow | serializer 显式调用 | Joi/zod 显式调用 |
| 自动文档 | 原生,与校验同源 | 需扩展 | drf-spectacular 较完善 | 注释生成 |
| 异步并发 | 原生 ASGI | 补充支持 | 演进中 | 原生事件循环 |
| 全家桶程度 | 仅 API 层 | 微框架 | ORM Admin 迁移齐备 | 微框架 |
| 典型代价 | 类型纪律要求 | 校验文档自理 | 重量、学习曲线 | 回调心智、类型靠 TS 补 |

选型讨论常常忽略迁移成本这一维度。从 Flask 迁到 FastAPI,概念映射其实是平滑的:app.route 换成带 HTTP 动词的装饰器、request.get_json 换成 Pydantic 模型参数、jsonify 换成直接返回 dict、before_request 钩子换成本章后面会讲的依赖注入或中间件。一个中等规模的服务(三四十个接口),熟悉 Flask 的工程师通常一两周可以完成主体迁移,因为业务逻辑层几乎不动,改的只是"边界层"——参数进来的方式和响应出去的方式。
真正的迁移风险不在语法而在两处。一是隐性行为差异:Flask 习惯了 type=int 静默回落、习惯了字典键不存在返回 None 再判空,这些"宽容"在 FastAPI 里都变成了 422 拒绝,如果上游真的在传脏数据,迁移当天就会暴露出来——这不是退步,而是把技术债一次性结清,但要预留出修数据的缓冲期。二是同步依赖的适配:如果旧服务里的 ORM、缓存客户端、消息队列 SDK 都是同步的,直接搬进 async 路由会把事件循环卡死,要么先用同步路由(FastAPI 会把 def 路由放进线程池,行为不劣于 Flask),要么分批替换为异步客户端。第 3 章会给出这条迁移路线的完整对照。
从 Django 迁移的情况较少见,因为两者定位重叠小:通常不是"迁移",而是新业务用 FastAPI 起独立服务,Django 继续管内容与后台,两者通过内部 HTTP 或消息队列协作。这种渐进式共存反而是微服务语境下最常见的落地形态。
生态维度需要说些实话。Flask 与 Django 的资料存量是十几年积累,中文社区的问答密度至今仍高于 FastAPI,遇到偏门问题搜到的答案更多。FastAPI 的优势领域集中在近几年新增的实践:异步数据库客户端、大模型推理服务封装、Serverless 部署、OpenAPI 工具链集成——这些场景下新教程与新组件反而以 FastAPI 为默认前提。招人角度,Python 工程师学 FastAPI 的曲线平缓(核心概念一周内可上手),而"会 FastAPI"在很大程度上等价于"会写带类型提示的现代 Python",这个技能本身在团队内有溢出价值。
组件生态方面要认清一个事实:FastAPI 官方仓库刻意保持小而精,登录认证、数据库会话、限流这些在别的框架里由官方或半官方扩展承担的东西,这里由 fastapi-users、sqlmodel 等独立项目或自己用依赖注入组合。依赖注入系统就是为此设计的——它把"粘合外部组件"这件事变成了框架的一等能力。接受这个设定后你会发现,FastAPI 项目里几乎没有"插件"概念,所有集成都用同一套依赖语法表达,心智反而更统一。
第一,重内容管理的站点。新闻站、电商后台、需要 Admin 界面和权限管理后台的项目,Django 一套脚手架顶 FastAPI 手拼两周。第二,CPU 密集为主的服务。图像处理、报表计算这类负载,异步帮不上忙,Python 的 GIL 才是瓶颈,选什么 Web 框架都一样,反而应该考虑把计算拆到独立 worker。第三,团队完全没有类型提示习惯。FastAPI 的红利全部建立在签名标注之上,没有标注的 FastAPI 代码只是一个需要自己写校验的普通框架,此时 Flask 的资料存量反而更友好。
补一句容易被忽略的第四种情况:强依赖特定同步生态的项目。如果团队的核心资产是一套深度绑定 WSGI 的中间件、认证体系或监控探针,迁移到 ASGI 意味着整条链路重写,此时框架层面的性能收益未必抵得过改造成本。技术选型的理性姿态是算总账,不是单点比参数。
type=int 回落默认值会吞错,FastAPI 的 422 显式拒绝,API 对外服务时后者更安全。选型定了,下一节动手:把虚拟环境、依赖与开发服务器搭起来,顺便对比一下 venv、virtualenv、conda 三套隔离工具该怎么选。