FastAPI 的一个重要优势是基于类型标注和 Pydantic 做参数校验。接口参数写清楚后,框架可以自动解析请求、校验字段、生成 OpenAPI 文档,并在参数错误时返回结构化响应。
本文通过一个用户创建接口,介绍路径参数、查询参数、请求体校验、自定义异常和统一错误返回的实践方式。
基础项目结构
一个简单项目可以这样组织:
1 2 3 4
| app/ main.py schemas.py exceptions.py
|
安装依赖:
1
| pip install fastapi uvicorn
|
启动命令:
1
| uvicorn app.main:app --reload
|
访问 http://127.0.0.1:8000/docs 可以查看自动生成的接口文档。
定义请求体模型
在 schemas.py 中定义用户创建请求:
1 2 3 4 5 6
| from pydantic import BaseModel, EmailStr, Field
class CreateUserRequest(BaseModel): username: str = Field(min_length=3, max_length=32) email: EmailStr age: int = Field(ge=0, le=150)
|
字段含义:
username 长度必须在 3 到 32 之间。
email 必须是合法邮箱格式。
age 必须在 0 到 150 之间。
如果使用 EmailStr,需要安装邮箱校验依赖:
1
| pip install "pydantic[email]"
|
响应模型也建议显式定义:
1 2 3 4
| class UserResponse(BaseModel): id: int username: str email: EmailStr
|
路径参数和查询参数
在 main.py 中创建接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| from fastapi import FastAPI, Query
from .schemas import CreateUserRequest, UserResponse
app = FastAPI()
@app.get("/users/{user_id}", response_model=UserResponse) def get_user( user_id: int, include_deleted: bool = Query(default=False), ): return { "id": user_id, "username": "demo", "email": "demo@example.com", }
|
user_id 会自动从路径中解析为整数。如果请求 /users/abc,FastAPI 会返回参数错误。
查询参数 include_deleted 会自动解析布尔值,例如:
1
| curl 'http://127.0.0.1:8000/users/1?include_deleted=true'
|
请求体校验
创建用户接口:
1 2 3 4 5 6 7
| @app.post("/users", response_model=UserResponse) def create_user(req: CreateUserRequest): return { "id": 1, "username": req.username, "email": req.email, }
|
测试请求:
1 2 3
| curl -X POST 'http://127.0.0.1:8000/users' \ -H 'Content-Type: application/json' \ -d '{"username":"clang","email":"clang@example.com","age":20}'
|
如果 username 过短或邮箱格式错误,FastAPI 会返回 422,并说明具体字段错误。
自定义业务异常
参数校验解决的是输入格式问题,业务错误还需要自己定义。例如用户不存在、用户名重复、权限不足。
在 exceptions.py 中定义:
1 2 3 4 5
| class AppError(Exception): def __init__(self, code: str, message: str, status_code: int = 400): self.code = code self.message = message self.status_code = status_code
|
在入口注册异常处理器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| from fastapi import Request from fastapi.responses import JSONResponse
from .exceptions import AppError
@app.exception_handler(AppError) async def app_error_handler(request: Request, exc: AppError): return JSONResponse( status_code=exc.status_code, content={ "code": exc.code, "message": exc.message, }, )
|
业务代码中抛出:
1 2 3 4 5 6 7 8 9 10
| @app.get("/users/{user_id}", response_model=UserResponse) def get_user(user_id: int): if user_id == 404: raise AppError("USER_NOT_FOUND", "user not found", 404)
return { "id": user_id, "username": "demo", "email": "demo@example.com", }
|
这样业务错误的返回结构就统一了。
覆盖参数校验错误格式
如果希望 422 参数错误也返回统一结构,可以处理 RequestValidationError:
1 2 3 4 5 6 7 8 9 10 11 12
| from fastapi.exceptions import RequestValidationError
@app.exception_handler(RequestValidationError) async def validation_error_handler(request: Request, exc: RequestValidationError): return JSONResponse( status_code=422, content={ "code": "VALIDATION_ERROR", "message": "request validation failed", "details": exc.errors(), }, )
|
details 中包含字段位置、错误类型和错误原因。对前端联调来说,这比只返回一段字符串更好处理。
不要吞掉未知异常
可以增加全局异常处理器,但不要把未知异常伪装成业务错误:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import logging
logger = logging.getLogger(__name__)
@app.exception_handler(Exception) async def unknown_error_handler(request: Request, exc: Exception): logger.exception("unhandled error path=%s", request.url.path) return JSONResponse( status_code=500, content={ "code": "INTERNAL_ERROR", "message": "internal server error", }, )
|
日志中保留堆栈,响应中不要暴露数据库地址、SQL、密钥、内部文件路径等敏感信息。
常见问题
字段没有生效时,先检查请求头是否为 Content-Type: application/json,以及请求体是否是合法 JSON。
路径参数、查询参数和请求体参数可以同时存在,但函数签名要清晰。复杂查询条件建议定义成单独模型或依赖函数。
返回模型不要直接暴露数据库 ORM 对象中的所有字段,尤其是密码哈希、手机号、内部状态位等敏感字段。
小结
FastAPI 的参数校验能力可以显著减少手写校验代码。实践中建议用 Pydantic 模型描述请求体和响应体,用自定义异常表达业务错误,用统一异常处理器规范错误结构。这样接口文档、后端逻辑和前端联调都会更稳定。