10分钟用FastAPI写出第一个Python API

10分钟用FastAPI写出第一个Python API

1. 项目概述:为什么“10分钟写出第一个Python API”不是标题党,而是真实可落地的工程起点

“III. Your First Python API in Under 10 Minutes”——这个标题乍看像极了那些被算法推上首页的速成类内容,点进去却发现全是环境安装卡在第一步、依赖报错堆满屏幕、最后用一个print("Hello World")假装完成了API。但作为过去十年里亲手搭过27个生产级API服务、从Flask轻量路由到FastAPI高并发网关都踩过坑的老手,我可以很确定地说:这个“10分钟”是真实计时的,而且它背后藏着一条被绝大多数新手忽略的关键分水岭——不是你会不会写@app.get("/"),而是你是否在敲下第一行代码前,就已默认接受了API的契约本质。

所谓API,从来不是“让程序能返回点东西”的技术动作,而是一份面向外部调用者的协议说明书。它规定了谁可以来、以什么格式来、带什么参数来、会得到什么样的结构化响应、出错了怎么理解错误码。这和你本地写个脚本自娱自乐有本质区别。所以这个项目真正的价值,不在于教会你某个框架的语法糖,而在于用最短路径让你建立起对HTTP语义、RESTful设计原则、请求-响应生命周期的肌肉记忆。我带过的实习生里,83%的人在学完Django REST Framework后依然搞不清400和422的区别,原因就是他们跳过了“第一个裸API”这个建立直觉的过程。

这个标题里的“III.”也很有意思——它暗示这不是孤立教程,而是某个系列中的第三部分。这意味着前两节大概率解决了Python基础环境统一(比如用pyenv管理多版本)、命令行与HTTP工具链认知(curl/postman/HTTPie的基本用法)。所以本文默认你已具备:能通过终端运行python --version并看到3.9+输出;知道pip install是干什么的;能用curl -X GET http://localhost:8000发起一次最简请求。如果你连这些都不确定,别急着往下翻,先花3分钟确认这三件事,否则后面所有“10分钟”都会变成“100分钟”。

适合谁来跟着做?第一类是刚学完Python语法、正站在Web开发门口张望的新手——你需要的不是一上来就讲JWT鉴权或异步数据库连接池,而是亲手把“用户发一个GET请求,服务器回一个JSON”这个闭环走通;第二类是转行做后端的测试/运维/数据分析人员,你们更需要快速理解API如何被消费,而不是深陷框架源码;第三类反而是有经验但长期用Java/Node.js的开发者,想用Python验证某个微服务接口逻辑,需要零负担启动一个可调试的stub服务。

核心关键词“Python API”“FastAPI”“RESTful”“curl”“JSON Response”全部指向一个事实:我们不做部署、不连数据库、不加中间件、不写单元测试——就聚焦在“让一个Python进程监听HTTP端口,并按规范返回结构化数据”这一件事上。所有延伸功能(如数据库集成、身份认证、文档自动生成)都是这个最小可行单元长出的枝叶,而非主干。这也是为什么我坚持用FastAPI而非Flask作为默认选择:它的类型提示即文档、自动OpenAPI生成、内置Pydantic校验,从第一行代码起就在潜移默化地训练你写符合契约的API,而不是先纵容你写出一堆return {"data": result, "status": "success"}这种反模式。

2. 整体设计思路拆解:为什么选FastAPI、为什么拒绝Flask、为什么必须用uvicorn

2.1 框架选型不是口味问题,而是工程约束的显性化表达

很多人看到“10分钟API”第一反应是:“用Flask几行代码搞定啊!”——确实,Flask的hello world只要4行:

from flask import Flask app = Flask(__name__) @app.route("/") def hello(): return "Hello World" app.run()

但问题在于:这段代码离真正可用的API还有多远?我们来逐行拆解它隐含的工程负债:

  • from flask import Flask:Flask本身不处理异步,当你要加个await asyncio.sleep(1)模拟数据库延迟时,整个进程就阻塞了;
  • @app.route("/"):路由定义不带类型声明,参数校验要手动写if not request.args.get("id").isdigit(),错误处理分散在各处;
  • return "Hello World":返回纯字符串,Content-Type默认是text/html,前端fetch时得手动设置responseType: 'text',而标准API该返回application/json
  • app.run():开发服务器不支持热重载(改代码要手动重启),生产环境绝对禁止用它(官方文档明确警告)。

这些不是“小问题”,而是当你把API交给前端同事联调时,对方第一句就会问:“为什么我的axios请求报Unexpected token H in JSON at position 0?”——因为你的return "Hello World"返回的是HTML文本,不是JSON对象。

FastAPI则把这些问题变成了编译期检查:

from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str price: float @app.post("/items/") def create_item(item: Item): return {"name": item.name, "price": item.price}

注意item: Item这个参数声明——它不只是注释,而是强制要求传入的JSON必须包含name(str)和price(float),否则直接返回422 Unprocessable Entity,并附带精确到字段的错误信息。这种“类型即契约”的设计,让API文档、参数校验、序列化三件事在一行代码里完成。我做过对比测试:同样实现一个带参数校验的用户注册接口,Flask需要23行代码(含手动解析JSON、类型转换、错误返回),FastAPI只需9行,且零配置就生成Swagger UI文档。

提示:不要被“FastAPI很新”吓住。它底层用的是Starlette(ASGI框架)和Pydantic(数据验证库),这两个组件在2018年就已在生产环境大规模使用。FastAPI更像是把最佳实践打包成开箱即用的方案,而非另起炉灶。

2.2 为什么必须用uvicorn,而不是FastAPI自带的run方法?

FastAPI文档里确实有uvicorn.run(app, host="0.0.0.0:8000")这样的写法,但实际项目中我坚决不用。原因很简单:uvicorn是独立进程,而FastAPI的run只是个薄包装,它掩盖了ASGI服务器的核心概念。

ASGI(Asynchronous Server Gateway Interface)是Python Web的现代标准,它定义了异步应用与服务器之间的通信协议。uvicorn是目前最成熟的ASGI服务器实现,它的优势在于:

  • 真正的异步支持:能同时处理数千个长连接(如WebSocket),而Flask的WSGI服务器(如Werkzeug)本质是同步的,靠多进程/多线程模拟并发;
  • 热重载开箱即用uvicorn main:app --reload,保存文件后服务自动重启,无需任何插件;
  • 生产就绪配置--workers 4 --limit-concurrency 1000等参数可直接用于Docker容器部署;
  • 进程管理友好:配合supervisord或systemd时,uvicorn的进程信号处理比混合包装更稳定。

我见过太多人用fastapi dev命令(这是FastAPI CLI工具)启动,结果在Docker里发现热重载失效、日志不输出、SIGTERM信号被忽略——因为fastapi dev本质是封装了uvicorn,但增加了额外抽象层。直接学uvicorn,等于一步到位掌握Python异步Web服务的基础设施。

2.3 为什么拒绝一切“高级功能”:数据库、ORM、认证、前端模板

这个“10分钟API”的设计哲学,是严格遵循YAGNI原则(You Aren't Gonna Need It)。新手最容易犯的错误,就是一上来就想“我的API要连MySQL”“要支持JWT登录”“要渲染HTML页面”。结果呢?花了2小时配SQLAlchemy,却连curl http://localhost:8000都返回404。

真正的API开发节奏应该是:

  1. 先让GET /返回{"message": "OK"}(2分钟)
  2. 再让POST /items接收JSON并返回原样(3分钟)
  3. 然后加Pydantic模型做字段校验(2分钟)
  4. 最后加uvicorn热重载(1分钟)
  5. 剩余2分钟,用curl实测所有路径

每一步都可独立验证,失败时能精准定位问题模块。而如果第一步就引入SQLAlchemy,那么当curl返回500时,你得排查:是路由没注册?是数据库连接串写错?是表结构不存在?还是Pydantic模型和DB字段不匹配?这种模糊性会直接摧毁学习信心。

我带团队时有个铁律:所有新成员入职第一周,必须用纯内存字典实现一个CRUD API(增删改查全走通),不许碰任何外部依赖。等他们能熟练用curl验证每个HTTP状态码(200/201/400/404/500)的含义后,再引入SQLite——这时他们才真正理解“数据库只是数据存储的一种方式,而非API的本质”。

3. 核心细节解析与实操要点:从零创建可验证的API服务

3.1 环境准备:三步确认法,避免90%的“环境问题”

很多教程失败的根本原因,不是代码写错,而是环境没对齐。我总结出一套三步确认法,每次新建项目必做:

第一步:确认Python版本与虚拟环境隔离
打开终端,执行:

python --version # 必须输出 3.9.0 或更高版本(FastAPI最低要求3.7,但3.9+对类型提示支持更好) which python # 输出应为类似 /Users/xxx/.pyenv/versions/3.11.5/bin/python,而非系统自带的/usr/bin/python

如果which python指向系统Python(尤其Mac用户),立刻创建隔离环境:

python -m venv myapi_env source myapi_env/bin/activate # Linux/Mac # myapi_env\Scripts\activate.bat # Windows

注意:绝对不要用sudo pip install!这会污染系统Python环境,导致后续包冲突。虚拟环境是Python项目的呼吸面罩,没有它,一切皆空谈。

第二步:安装核心依赖并验证可导入
在激活的虚拟环境中执行:

pip install "fastapi[all]" uvicorn # [all] 选项会额外安装Uvicorn、Pydantic、Jinja2(用于模板)、aiofiles(异步文件读写)等常用扩展

安装完成后,立即验证:

python -c "import fastapi; print(fastapi.__version__)" python -c "import uvicorn; print(uvicorn.__version__)"

如果报ModuleNotFoundError,说明pip安装路径和python解释器不一致——常见于VS Code未正确加载虚拟环境。此时在VS Code中按Cmd+Shift+P(Mac)或Ctrl+Shift+P(Win),输入“Python: Select Interpreter”,手动选择myapi_env/bin/python

第三步:创建最小可运行文件并测试HTTP服务
新建文件main.py,写入最简代码:

from fastapi import FastAPI app = FastAPI() @app.get("/") def read_root(): return {"Hello": "World"}

保存后,在终端执行:

uvicorn main:app --reload --host 0.0.0.0 --port 8000

关键参数解析:

  • main:app:冒号前是文件名(不含.py),冒号后是FastAPI实例变量名
  • --reload:启用热重载,文件保存后自动重启
  • --host 0.0.0.0:允许外部设备访问(如手机浏览器访问本机IP)
  • --port 8000:指定端口,避免与Node.js的3000、Docker的5000冲突

启动成功后,终端会显示:

INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit) INFO: Started reloader process [12345] INFO: Started server process [12346] INFO: Waiting for application startup. INFO: Application startup complete.

此时打开浏览器访问http://localhost:8000,或执行curl http://localhost:8000,应返回:

{"Hello":"World"}

如果返回Connection refused,检查:

  • 终端是否还在运行uvicorn进程(别误关了)
  • 防火墙是否阻止8000端口(Mac用户注意“系统偏好设置→安全性与隐私→防火墙”)
  • 是否在Docker容器内运行却忘了-p 8000:8000端口映射

3.2 构建真实API:从“Hello World”到可交互的RESTful端点

现在我们把“Hello World”升级为一个真实的资源操作API。假设我们要提供一个/items端点,支持:

  • GET /items:获取所有商品列表
  • GET /items/{item_id}:根据ID获取单个商品
  • POST /items:创建新商品
  • PUT /items/{item_id}:更新商品信息

第一步:定义数据模型(Pydantic)
main.py顶部添加:

from pydantic import BaseModel from typing import List, Optional class Item(BaseModel): id: int name: str price: float is_offer: Optional[bool] = None # 可选字段,默认None

这里的关键细节:

  • id: int:类型提示强制要求传入数字,字符串"1"会被自动转为1"abc"则直接报422错误
  • Optional[bool]:表示该字段可传可不传,若不传则值为None,避免前端必须填满所有字段
  • BaseModel继承:赋予对象.dict()方法(转字典)、.json()方法(转JSON字符串)、自动校验能力

第二步:实现内存数据库(仅用于演示)
为保持零依赖,我们用Python字典模拟数据库:

# 内存数据库(实际项目中替换为SQLAlchemy或MongoDB) fake_items_db = [ {"id": 1, "name": "Book", "price": 12.99, "is_offer": False}, {"id": 2, "name": "Pen", "price": 2.50, "is_offer": True}, ]

第三步:编写四个端点(完整代码)
将以下代码追加到main.py

@app.get("/items/", response_model=List[Item]) def read_items(): return fake_items_db @app.get("/items/{item_id}", response_model=Item) def read_item(item_id: int): for item in fake_items_db: if item["id"] == item_id: return item return {"error": "Item not found"} # 实际项目应抛异常 @app.post("/items/", response_model=Item) def create_item(item: Item): fake_items_db.append(item.dict()) return item @app.put("/items/{item_id}", response_model=Item) def update_item(item_id: int, item: Item): for i, existing in enumerate(fake_items_db): if existing["id"] == item_id: fake_items_db[i] = item.dict() return item return {"error": "Item not found"}

关键参数说明:

  • response_model=List[Item]:告诉FastAPI返回值是Item列表,自动生成OpenAPI文档中的数组结构,并做响应体校验
  • item_id: int:路径参数类型声明,/items/abc会直接返回422,无需手动判断
  • item: Item:请求体自动解析JSON并校验,{"name": "test", "price": "not_a_number"}会报错

第四步:用curl实测所有端点
打开新终端窗口(不要关uvicorn),依次执行:

# 获取所有商品 curl http://localhost:8000/items/ # 获取ID为1的商品 curl http://localhost:8000/items/1 # 创建新商品(注意JSON引号需转义) curl -X POST http://localhost:8000/items/ \ -H "Content-Type: application/json" \ -d '{"id": 3, "name": "Notebook", "price": 5.99}' # 更新商品价格 curl -X PUT http://localhost:8000/items/1 \ -H "Content-Type: application/json" \ -d '{"id": 1, "name": "Book", "price": 15.99}'

每次执行后,观察终端uvicorn日志,确认HTTP状态码(200/201)和返回内容。你会发现:

  • POST传入"price": "abc"时,FastAPI自动返回422错误,且响应体明确指出"price" is not a valid number
  • GET /items/999时,返回{"error": "Item not found"},但状态码仍是200——这不符合RESTful规范,我们稍后会修复。

实操心得:永远用curl测试,而不是只信浏览器。浏览器GET请求简单,但POST/PUT需要构造JSON头,curl能暴露所有HTTP细节。我习惯把常用curl命令写成shell脚本(如test_api.sh),每次改代码后一键运行,比点鼠标快十倍。

3.3 关键配置与安全加固:让API不止于“能跑”

一个能跑的API和一个可交付的API之间,隔着几个关键配置。以下是我在生产环境强制启用的三项:

1. 启用CORS(跨域资源共享)
前端在http://localhost:3000运行,API在http://localhost:8000,浏览器会因同源策略拦截请求。FastAPI提供CORSMiddleware一键解决:

from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins=["http://localhost:3000"], # 允许的前端地址 allow_credentials=True, # 允许携带cookie allow_methods=["*"], # 允许所有HTTP方法 allow_headers=["*"], # 允许所有请求头 )

注意:allow_origins=["*"]在生产环境绝对禁止!必须精确指定前端域名,否则任何网站都能调用你的API。

2. 自定义错误处理(让404/500更友好)
当前GET /items/999返回200状态码加错误消息,这是严重反模式。RESTful规范要求:资源不存在必须返回404。修改read_item函数:

from fastapi import HTTPException @app.get("/items/{item_id}", response_model=Item) def read_item(item_id: int): for item in fake_items_db: if item["id"] == item_id: return item raise HTTPException(status_code=404, detail="Item not found")

同理,update_item也应改为raise HTTPException(404, "Item not found")。这样前端收到404时,就能触发错误处理逻辑,而不是误以为操作成功。

3. 添加健康检查端点(运维友好)
Kubernetes、Docker Swarm等编排工具需要定期探测服务是否存活。添加一个无业务逻辑的端点:

@app.get("/healthz") def health_check(): return {"status": "ok", "timestamp": datetime.now().isoformat()}

需先导入from datetime import datetime。此端点不涉及任何业务代码,即使数据库挂了,它仍能返回200,让运维知道“服务进程活着,只是依赖不可用”。

4. 实操过程与核心环节实现:从启动到联调的完整流水线

4.1 完整项目结构与文件组织

一个可维护的API项目,绝不能只有一个main.py。我推荐的最小结构如下:

myapi/ ├── main.py # FastAPI应用入口 ├── models.py # Pydantic数据模型定义 ├── database.py # 数据库连接/操作(当前为空白占位) ├── requirements.txt # 依赖清单 └── README.md # 项目说明

models.py内容(解耦数据模型)

from pydantic import BaseModel from typing import Optional, List class ItemBase(BaseModel): name: str price: float class ItemCreate(ItemBase): pass class Item(ItemBase): id: int is_offer: Optional[bool] = None class Config: orm_mode = True # 允许从SQLAlchemy ORM对象直接构建

requirements.txt生成(确保环境可复现)
在虚拟环境中执行:

pip freeze > requirements.txt

内容示例:

fastapi==0.110.0 uvicorn==0.29.0 pydantic==2.7.1

提示:生产环境务必锁定版本号(如fastapi==0.110.0),避免fastapi>=0.100.0导致意外升级破坏兼容性。

4.2 使用Postman进行可视化联调(替代curl的进阶方案)

虽然curl是工程师的瑞士军刀,但当API变复杂时,Postman能极大提升效率。以下是配置步骤:

  1. 下载安装Postman(免费版足够)
  2. 新建Collection命名为“My API”
  3. 在Collection中创建Request:
    • Name:Get All Items
    • Method:GET
    • URL:http://localhost:8000/items/
  4. 点击右上角“Save Response”图标,保存返回的JSON为示例(Example)
  5. POST /items/创建Request,切换到Body→raw→JSON,粘贴:
{ "id": 4, "name": "Eraser", "price": 1.25 }
  1. 发送后,点击“Save Response”保存成功创建的Item为Example

这样做的好处:

  • 团队协作时,直接分享Postman Collection链接,新人无需看文档就能试用所有API;
  • 每个Example都记录了请求头、参数、响应体、状态码,比curl命令更直观;
  • 后续添加认证后,可在Authorization标签页统一配置Bearer Token,所有Request自动携带。

我坚持要求团队所有API必须提供Postman Collection,因为“能被Postman调通”是API可用性的最低门槛。

4.3 自动生成API文档:Swagger UI与ReDoc双引擎

FastAPI最惊艳的特性之一,是零配置生成专业级API文档。启动服务后,直接访问:

  • http://localhost:8000/docs→ Swagger UI(交互式文档,可直接发送请求)
  • http://localhost:8000/redoc→ ReDoc(更简洁的阅读视图,适合嵌入Wiki)

文档内容完全由代码注释和类型提示生成。例如给create_item添加文档字符串:

@app.post("/items/", response_model=Item, summary="Create a new item") def create_item(item: Item): """ Create an item with all the information: - **name**: each item must have a name - **price**: required and must be a float - **is_offer**: optional boolean field """ fake_items_db.append(item.dict()) return item

Swagger UI会自动将docstring渲染为描述,summary参数显示为接口标题。更进一步,可以用description参数写更长的说明,用tags=["items"]对端点分组。

实操心得:文档即代码。我见过太多团队把API文档写在Confluence里,结果代码改了文档没同步,前端按过时文档联调失败。FastAPI的方案彻底消灭了这种割裂——你改代码,文档自动更新,这才是现代API开发该有的样子。

4.4 日志与调试:让问题无所遁形

生产环境必须记录请求详情。FastAPI默认日志级别是INFO,但我们需要更细粒度的控制。在main.py顶部添加:

import logging from fastapi import Request # 配置日志 logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", handlers=[logging.StreamHandler()] ) logger = logging.getLogger(__name__) # 中间件记录请求 @app.middleware("http") async def log_requests(request: Request, call_next): logger.info(f"Request: {request.method} {request.url.path}") response = await call_next(request) logger.info(f"Response status: {response.status_code}") return response

启动后,每次请求都会在终端打印:

2024-05-20 14:23:45,123 - __main__ - INFO - Request: GET /items/ 2024-05-20 14:23:45,125 - __main__ - INFO - Response status: 200

这对排查问题至关重要。比如前端说“调用POST接口没反应”,你看日志发现根本没有Request: POST记录,那问题一定出在前端网络请求配置上,而非后端代码。

5. 常见问题与排查技巧实录:那些没人告诉你但每天都在发生的坑

5.1 “ImportError: cannot import name 'XXX'” —— 版本地狱的典型症状

现象:安装FastAPI后,运行python main.py报错ImportError: cannot import name 'Field' from 'pydantic'

原因:Pydantic v2(FastAPI 0.100+要求)和v1(旧版FastAPI使用)API不兼容。Field在v2中移到了pydantic.fields,而v1中在pydantic根命名空间。

排查步骤

  1. 执行pip show pydantic,查看Version字段
  2. 如果是2.x.x,而你的代码还用着from pydantic import Field,则需改为:
from pydantic import BaseModel from pydantic.fields import Field # v2写法 # 或更推荐:用类型注解替代Field class Item(BaseModel): name: str = Field(..., min_length=1) # ...表示必填
  1. 如果是1.x.x,执行pip install "pydantic>=2.0.0"强制升级

经验:永远用pip show <package>确认实际安装版本,不要相信requirements.txt里的注释。我有个脚本check_deps.sh,每次拉新代码后自动运行:pip show fastapi pydantic uvicorn | grep -E "(Name|Version)"

5.2 “TypeError: Object of type set is not JSON serializable” —— JSON序列化的隐形杀手

现象:返回{"tags": {"python", "fastapi"}}时报错,但{"tags": ["python", "fastapi"]}正常。

原因:Python的set类型无法被json.dumps()序列化,FastAPI默认用jsonable_encoder处理响应,但它不支持set。

解决方案

  • 方案1(推荐):改用listtuple,它们是JSON标准类型
  • 方案2:自定义JSON编码器
from fastapi.encoders import jsonable_encoder from fastapi.responses import JSONResponse @app.get("/items/") def read_items(): items = [{"id": 1, "tags": {"python", "fastapi"}}] # 手动转换set为list for item in items: if "tags" in item: item["tags"] = list(item["tags"]) return JSONResponse(content=jsonable_encoder(items))

5.3 “422 Unprocessable Entity”但找不到具体错误字段 —— 调试JSON校验的黄金法则

现象curl -X POST返回422,但响应体只显示{"detail": [...]},看不出哪个字段错了。

原因:FastAPI的422错误详情默认只在DEBUG模式下显示完整路径。

解决方法

  1. 启动时加--debug参数:uvicorn main:app --reload --debug
  2. 或在代码中启用详细错误:
app = FastAPI(debug=True) # 开发环境开启

此时422响应体变为:

{ "detail": [ { "loc": ["body", "price"], "msg": "value is not a valid number", "type": "type_error.number" } ] }

loc字段明确指出错误在请求体(body)的price字段,msg说明是类型错误。

实操心得:永远在开发环境开启debug=True,上线前再设为False。我见过太多人因忽略这点,在联调时对着422错误抓耳挠腮两小时。

5.4 “Connection refused”但uvicorn明明在运行 —— 网络栈排查四步法

现象:终端显示uvicorn已启动,但curl http://localhost:8000返回Connection refused

系统性排查

  1. 确认进程存活ps aux | grep uvicorn,检查是否有uvicorn main:app进程
  2. 确认端口占用lsof -i :8000(Mac/Linux)或netstat -ano | findstr :8000(Windows),看是否被其他程序占用
  3. 确认绑定地址:uvicorn日志中Uvicorn running on http://0.0.0.0:8000表示监听所有IP,若显示http://127.0.0.1:8000则只能本机访问
  4. 确认防火墙:临时关闭防火墙测试(Mac:sudo pfctl -d;Windows:关闭“Windows Defender 防火墙”)

终极方案:用telnet localhost 8000测试端口连通性。如果telnet未安装,用nc -zv localhost 8000(Linux/Mac)或下载telnet.exe(Win)。只要telnet能连上,说明服务正常,问题一定出在HTTP客户端(curl/浏览器)配置上。

5.5 “POST请求接收不到JSON数据” —— Content-Type的生死线

现象:前端用fetch发送JSON,但FastAPI的item: Item参数始终为空。

原因:前端未设置Content-Type: application/json头,或FastAPI未正确解析。

验证方法

  1. 用curl发送带头的请求:
curl -X POST http://localhost:8000/items/ \ -H "Content-Type: application/json" \ -d '{"name": "Test", "price": 10.0}'
  1. 如果成功,说明问题在前端;如果失败,检查FastAPI代码是否漏了response_model或类型声明

前端修复示例(JavaScript)

fetch('http://localhost:8000/items/', { method: 'POST', headers: { 'Content-Type': 'application/json', // 这行绝对不能少! }, body: JSON.stringify({name: "Test", price: 10.0}) })

注意:fetch默认不发送Content-Type头,必须显式声明。这是前端调用Python API时最常踩的坑,没有之一。

6. 后续演进路径:从“第一个API”到生产就绪服务的五级台阶

完成这个10分钟项目,只是万里长征第一步。根据我维护27个API服务的经验,一个API从玩具到生产,通常经历五个明确阶段。每个阶段都有清晰的验收标准,你可以对照自查:

阶段关键任务验收标准典型耗时
Level 1:可运行能用curl调通所有端点,返回正确JSON和状态码curl -s http://localhost:8000/items/ | jq .输出格式化JSON,无HTML乱码10分钟(当前项目)
Level 2:可测试编写pytest单元测试,覆盖所有端点的成功/失败路径pytest tests/ --tb=short全部通过,覆盖率≥80%2小时
Level 3:可部署Docker镜像构建成功,docker run -p 8000:8000 myapi可访问docker build -t myapi . && docker run -p 8000:8000 myapi启动后curl返回正常1小时
Level 4:可监控集成Prometheus指标,Grafana看板显示QPS、延迟、错误率Grafana中能看到http_requests_total{path="/items/"}实时曲线3小时
Level 5:可演进支持灰度发布(如/v1/items//v2/items/共存),API变更有版本迁移计划新老版本同时在线,前端可自由切换,无停机升级1天

你现在站在Level 1的终点线。下一步建议:

  • 立刻为main.py写第一个pytest测试(测试GET /items/返回200);
  • fake_items_db替换为SQLite(pip install aiosqlite),体验真正的数据持久化;
  • requirements.txt中添加`