一个API最容易被忽略的问题就是:
客户端传过来的数据真的可靠吗?
答案通常是否定的。
前端可能传错类型,第三方客户端可能直接构造请求,甚至有人故意发异常数据。
所以后端不能相信客户端传来的任何数据。
最简单的参数校验
假设要创建用户:
@app.post("/users")
async def create_user(
name: str,
age: int
):
...FastAPI可以根据类型提示做基础验证。
比如:
age=18能正常处理。
如果传:
age=abc就会产生验证错误。
但实际项目里的数据通常比两个参数复杂得多。
使用Pydantic Model
比如:
from pydantic import BaseModel
class UserCreate(BaseModel):
name: str
age: int
email: str然后:
@app.post("/users")
async def create_user(user: UserCreate):
return user请求:
{
"name": "Tom",
"age": 18,
"email": "[email protected]"
}FastAPI会自动做数据解析和验证。
字段约束
Pydantic不只会检查类型。
还能加约束。
比如:
from pydantic import BaseModel, Field
class UserCreate(BaseModel):
name: str = Field(min_length=1, max_length=50)
age: int = Field(ge=0, le=150)这样:
name不能为空。
而:
age必须在合理范围内。
这类校验最好放在数据模型层,而不是散落在业务代码里。
Email校验
Pydantic还可以配合更具体的类型。
比如:
from pydantic import BaseModel
from pydantic.networks import EmailStr
class UserCreate(BaseModel):
email: EmailStr这样API收到数据之后能自动检查邮箱格式。
比自己写:
if '@' not in email:
...要可靠得多。
嵌套对象
实际API经常有嵌套结构:
{
"name": "Tom",
"profile": {
"age": 18,
"city": "Taipei"
}
}可以直接定义:
class Profile(BaseModel):
age: int
city: str
class UserCreate(BaseModel):
name: str
profile: Profile数据结构一目了然。
请求模型和响应模型分开
我比较建议不要所有接口都复用同一个Model。
比如:
class UserCreate(BaseModel):
name: str
password: str数据库返回:
class UserResponse(BaseModel):
id: int
name: str这样密码不会意外出现在响应里。
这其实是很重要的安全习惯。
别因为:
User这个模型"看起来能复用",就把数据库对象原样丢给客户端。
Pydantic让接口成为一种契约
前后端开发里一个很烦人的问题就是:
"这个接口到底返回什么?"
如果后端代码里有明确的Pydantic Model,那接口的数据结构就很清楚了。
比如:
class UserResponse(BaseModel):
id: int
name: str前端能明确知道:
id → number
name → string这其实就相当于建立了一种接口契约。
数据校验应该放在哪
我比较倾向于把不同类型的校验放在不同层。
请求格式
↓
Pydantic
↓
业务规则
↓
Service
↓
数据库比如:
age必须是整数属于数据格式校验。
而:
用户年龄必须满足某项业务条件属于业务规则。
两者别完全混在一起。
别只信前端校验
前端当然也应该做校验。
比如:
邮箱格式
密码长度
必填字段这样用户体验更好。
但后端仍然必须重新校验。
因为:
前端校验 ≠ 安全边界任何客户端都能绕过前端代码直接调API。
FastAPI + Pydantic的组合
FastAPI和Pydantic放在一起之后,我觉得最大的价值就是:
请求
↓
类型解析
↓
数据验证
↓
业务代码很多重复的参数检查不用自己手写了。
同时类型定义本身又能成为API文档的一部分。
对于我这种经常要快速写API的场景来说,这种开发体验很舒服。
后面如果继续做FastAPI项目,我也会尽量让API的输入和输出模型保持明确,而不是让各种dict在项目里到处传。