FastAPI 核心语法

0 阅读9分钟

这篇只讲 FastAPI 写接口时最常用的语法。

先把主线记住:

创建 app
  -> 定义路由
  -> 接收参数
  -> 用 Pydantic 校验请求体
  -> 返回响应
  -> 抛出异常
  -> 复用依赖
  -> 拆分 Router

FastAPI 的核心不是“装饰器很多”,而是:

Python 类型标注
  -> FastAPI 读取类型
  -> Pydantic 做数据校验
  -> 自动生成 OpenAPI 文档
  -> 自动返回 JSON 响应

也就是说,接口规则很多时候不是靠你手写 if 判断,而是写在函数参数、类型注解和 Pydantic 模型里。

一、安装和启动

新项目如果使用 uv,可以这样安装:

uv add "fastapi[standard]"

fastapi[standard] 会安装 FastAPI 以及官方推荐的标准依赖,比如开发服务、表单、文件上传等常见能力需要的包。

如果只是临时学习,也可以用 pip

pip install "fastapi[standard]"

创建 main.py

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def read_root() -> dict[str, str]:
    return {
        "message": "Hello FastAPI",
    }

启动开发服务:

uv run fastapi dev main.py

如果已经激活虚拟环境,也可以直接:

fastapi dev main.py

访问:

http://127.0.0.1:8000
http://127.0.0.1:8000/docs

/docs 是 FastAPI 根据代码自动生成的交互式接口文档。

也可以用更底层的 Uvicorn 启动:

uv run uvicorn main:app --reload

这里的 main:app 可以这样理解:

main
  -> main.py 文件

app
  -> main.py 里的 FastAPI 实例

二、创建应用

FastAPI 项目通常从创建应用实例开始:

from fastapi import FastAPI

app = FastAPI()

可以给应用补充一些文档信息:

app = FastAPI(
    title="Todo API",
    description="一个用于学习 FastAPI 的接口服务",
    version="1.0.0",
)

这些信息会展示在 /docs 和 OpenAPI Schema 里。

三、定义路由

FastAPI 使用装饰器定义路由。

@app.get("/users")
def list_users() -> list[dict[str, object]]:
    return [
        {
            "id": 1,
            "name": "Alice",
        }
    ]

这段代码表示:

当请求 GET /users 时
  -> 执行 list_users 函数
  -> 把返回值转成 JSON 响应

常见 HTTP 方法:

@app.get("/users")
def list_users():
    return []


@app.post("/users")
def create_user():
    return {"id": 1}


@app.put("/users/{user_id}")
def update_user(user_id: int):
    return {"id": user_id}


@app.patch("/users/{user_id}")
def patch_user(user_id: int):
    return {"id": user_id}


@app.delete("/users/{user_id}")
def delete_user(user_id: int):
    return {"deleted": True}

先按 REST 习惯记:

GET
  -> 查询

POST
  -> 创建

PUT
  -> 全量更新

PATCH
  -> 局部更新

DELETE
  -> 删除

四、路径参数

路径参数写在 URL 路径里,用 {} 表示。

@app.get("/users/{user_id}")
def get_user(user_id: int) -> dict[str, int]:
    return {
        "id": user_id,
    }

请求:

GET /users/100

FastAPI 会把路径里的 100 传给函数参数 user_id

因为你写了:

user_id: int

所以 FastAPI 会尝试把字符串 "100" 转成整数 100

如果访问:

GET /users/abc

FastAPI 会自动返回参数校验错误。

多个路径参数:

@app.get("/users/{user_id}/orders/{order_id}")
def get_user_order(user_id: int, order_id: int) -> dict[str, int]:
    return {
        "user_id": user_id,
        "order_id": order_id,
    }

路径里的参数名必须和函数参数名对应。

如果要给路径参数加规则,用 Path

from typing import Annotated

from fastapi import Path


@app.get("/users/{user_id}")
def get_user(
    user_id: Annotated[int, Path(ge=1)],
) -> dict[str, int]:
    return {
        "id": user_id,
    }

含义:

user_id 来自路径
类型是 int
值必须大于等于 1

五、查询参数

查询参数就是 URL ? 后面的参数。

@app.get("/users")
def list_users(page: int = 1, size: int = 20) -> dict[str, int]:
    return {
        "page": page,
        "size": size,
    }

请求:

GET /users?page=2&size=10

对应:

page = 2
size = 10

FastAPI 的判断规则可以先这样记:

参数出现在路径里
  -> 路径参数

参数没有出现在路径里,并且是简单类型(str, int, float, bool)
  -> 查询参数

如果参数可以不传,常用 None

@app.get("/users")
def list_users(keyword: str | None = None) -> dict[str, object]:
    return {
        "keyword": keyword,
    }

str | None = None 表示:

keyword 可以是字符串
keyword 也可以是 None
请求里不传时默认是 None

Python 没有 JavaScript 里的 undefined。接口里表达“没有值”,通常就是 None

六、查询参数校验

如果只是写:

page: int = 1

只能表达类型和默认值。

如果还想表达最小值、最大值、长度限制、文档描述,可以用 Query

from typing import Annotated

from fastapi import Query


@app.get("/users")
def list_users(
    page: Annotated[int, Query(ge=1)] = 1,
    size: Annotated[int, Query(ge=1, le=100)] = 20,
    keyword: Annotated[str | None, Query(max_length=20)] = None,
) -> dict[str, object]:
    return {
        "page": page,
        "size": size,
        "keyword": keyword,
    }

先把 Annotated 理解成“给类型补充额外规则”。

Annotated[int, Query(ge=1)]
  -> 参数类型是 int
  -> FastAPI 额外要求这个查询参数 >= 1

常见校验规则:

参数含义
ge大于等于
gt大于
le小于等于
lt小于
min_length最小长度
max_length最大长度
pattern正则匹配

例如:

keyword: Annotated[str | None, Query(min_length=2, max_length=20)] = None

表示:

keyword 可以不传
如果传了,长度必须在 2 到 20 之间

七、请求体

请求体通常用 Pydantic 模型描述。

from pydantic import BaseModel


class CreateUserRequest(BaseModel):
    name: str
    age: int
    email: str | None = None


@app.post("/users")
def create_user(data: CreateUserRequest) -> dict[str, object]:
    return {
        "id": 1,
        "name": data.name,
        "age": data.age,
        "email": data.email,
    }

请求 JSON:

{
  "name": "Alice",
  "age": 18
}

FastAPI 会自动做这些事:

读取请求体 JSON
  -> 用 CreateUserRequest 校验字段
  -> 校验通过后创建 data 对象
  -> 把 data 传给 create_user 函数

Pydantic 模型里,没有默认值就是必填字段:

class CreateUserRequest(BaseModel):
    name: str
    age: int
    email: str | None = None
    is_active: bool = True

含义:

name 必传
age 必传
email 可不传,默认 None
is_active 可不传,默认 True

字段校验用 Field

from pydantic import BaseModel, Field


class CreateUserRequest(BaseModel):
    name: str = Field(min_length=2, max_length=20)
    age: int = Field(ge=0, le=150)
    email: str | None = None

可以这样理解:

Query
  -> 校验查询参数

Path
  -> 校验路径参数

Field
  -> 校验 Pydantic 模型字段

Pydantic v2 常用 model_dump() 转成普通字典:

@app.post("/users")
def create_user(data: CreateUserRequest) -> dict[str, object]:
    payload = data.model_dump()

    return {
        "id": 1,
        **payload,
    }

八、响应数据

FastAPI 可以直接返回 Python 数据结构。

@app.get("/users/{user_id}")
def get_user(user_id: int) -> dict[str, object]:
    return {
        "id": user_id,
        "name": "Alice",
    }

常见返回值:

return {"id": 1, "name": "Alice"}
return [{"id": 1}, {"id": 2}]
return {"success": True}

FastAPI 会把这些数据序列化成 JSON 响应。

响应结构建议用 Pydantic 模型表达:

from pydantic import BaseModel


class UserResponse(BaseModel):
    id: int
    name: str
    email: str | None = None


@app.get("/users/{user_id}", response_model=UserResponse)
def get_user(user_id: int) -> dict[str, object]:
    return {
        "id": user_id,
        "name": "Alice",
        "password": "should_not_return",
    }

response_model 有几个作用:

  • 生成接口文档
  • 校验响应数据
  • 过滤未声明字段
  • 让接口返回结构更清楚

上面例子里,password 不在 UserResponse 中,所以最终不会返回给前端。

如果函数返回值类型就是 Pydantic 模型,也可以直接写返回类型:

@app.get("/users/{user_id}")
def get_user(user_id: int) -> UserResponse:
    return UserResponse(
        id=user_id,
        name="Alice",
    )

状态码可以用 status 常量:

from fastapi import status


@app.post("/users", status_code=status.HTTP_201_CREATED)
def create_user(data: CreateUserRequest) -> dict[str, object]:
    return {
        "id": 1,
        **data.model_dump(),
    }

九、异常处理

接口里如果要主动返回错误状态,可以抛出 HTTPException

from fastapi import HTTPException


@app.get("/users/{user_id}")
def get_user(user_id: int) -> dict[str, object]:
    user = None

    if user is None:
        raise HTTPException(
            status_code=404,
            detail="用户不存在",
        )

    return user

常见状态:

raise HTTPException(status_code=400, detail="参数错误")
raise HTTPException(status_code=401, detail="未登录")
raise HTTPException(status_code=403, detail="无权限")
raise HTTPException(status_code=404, detail="资源不存在")

如果要统一业务异常格式,可以定义自定义异常处理器:

from fastapi import Request
from fastapi.responses import JSONResponse


class BusinessError(Exception):
    def __init__(self, message: str) -> None:
        self.message = message


@app.exception_handler(BusinessError)
def business_error_handler(
    request: Request,
    exc: BusinessError,
) -> JSONResponse:
    return JSONResponse(
        status_code=400,
        content={
            "code": "BUSINESS_ERROR",
            "message": exc.message,
        },
    )

业务代码里就可以:

raise BusinessError("余额不足")

十、依赖注入 Depends

Depends 是 FastAPI 里非常重要的语法。

它的作用是:把公共逻辑声明成当前接口的依赖。

常见场景:

  • 获取当前登录用户
  • 校验权限
  • 创建数据库会话
  • 复用分页参数
  • 解析公共请求头
  • 读取租户、部门、校区等上下文

示例:

from typing import Annotated

from fastapi import Depends, Header, HTTPException
from pydantic import BaseModel


class CurrentUser(BaseModel):
    id: int
    name: str


def get_current_user(
    authorization: Annotated[str | None, Header()] = None,
) -> CurrentUser:
    if authorization is None:
        raise HTTPException(status_code=401, detail="未登录")

    return CurrentUser(id=1, name="Alice")


@app.get("/profile")
def get_profile(
    current_user: Annotated[CurrentUser, Depends(get_current_user)],
) -> CurrentUser:
    return current_user

执行过程:

请求 GET /profile
  -> FastAPI 发现 current_user 依赖 get_current_user
  -> 先执行 get_current_user
  -> 从 Header 里读取 authorization
  -> 返回 CurrentUser
  -> 把 CurrentUser 传给 get_profile

Depends 的重点是声明关系:

这个接口需要 current_user
current_user 由 get_current_user 生成

接口函数不用关心当前用户怎么解析,只要声明自己需要它。

十一、Header、Cookie、Form、File

FastAPI 默认能推断路径参数、查询参数和请求体。

但 Header、Cookie、表单、文件上传这些来源需要明确声明。

Header

from typing import Annotated

from fastapi import Header


@app.get("/headers")
def read_headers(
    user_agent: Annotated[str | None, Header()] = None,
) -> dict[str, object]:
    return {
        "user_agent": user_agent,
    }

FastAPI 会把 HTTP 请求头里的 User-Agent 映射到 Python 参数 user_agent

Cookie

from typing import Annotated

from fastapi import Cookie


@app.get("/cookies")
def read_cookies(
    session_id: Annotated[str | None, Cookie()] = None,
) -> dict[str, object]:
    return {
        "session_id": session_id,
    }

Form

from typing import Annotated

from fastapi import Form


@app.post("/login")
def login(
    username: Annotated[str, Form()],
    password: Annotated[str, Form()],
) -> dict[str, str]:
    return {
        "username": username,
    }

File

from typing import Annotated

from fastapi import File, UploadFile


@app.post("/upload")
async def upload_file(
    file: Annotated[UploadFile, File()],
) -> dict[str, object]:
    content = await file.read()

    return {
        "filename": file.filename,
        "size": len(content),
    }

先记规律:

Header()
  -> 从请求头取

Cookie()
  -> 从 Cookie 取

Form()
  -> 从表单字段取

File()
  -> 从上传文件取

十二、同步函数和异步函数

FastAPI 支持普通函数:

@app.get("/sync")
def sync_api() -> dict[str, str]:
    return {
        "mode": "sync",
    }

也支持异步函数:

@app.get("/async")
async def async_api() -> dict[str, str]:
    return {
        "mode": "async",
    }

不要简单理解成“FastAPI 接口必须全部写 async”。

更实际的判断:

调用同步库
  -> def

调用异步库,并且需要 await
  -> async def

例如:

@app.get("/users")
async def list_users() -> list[dict[str, object]]:
    users = await user_repository.find_many()
    return users

如果你写了 async def,里面却调用大量阻塞式同步代码,性能不一定更好。

async def 的价值主要在 I/O 等待:

  • 异步数据库查询
  • 异步 HTTP 请求
  • WebSocket
  • 流式响应
  • 文件或网络 I/O

十三、Router 拆分

项目小的时候,所有代码可以先写在 main.py

项目变大后,应该拆分 Router。

app/api/v1/routers/users.py

from fastapi import APIRouter
from pydantic import BaseModel

router = APIRouter(
    prefix="/users",
    tags=["users"],
)


class CreateUserRequest(BaseModel):
    name: str
    age: int


@router.get("")
def list_users() -> list[dict[str, object]]:
    return []


@router.post("")
def create_user(data: CreateUserRequest) -> dict[str, object]:
    return {
        "id": 1,
        **data.model_dump(),
    }

app/main.py

from fastapi import FastAPI

from app.api.v1.routers import users

app = FastAPI()

app.include_router(users.router)

常见项目结构:

app/
  main.py
  api/
    v1/
      routers/
        users.py
        auth.py
  schemas/
    user.py
  services/
    user_service.py
  dependencies/
    auth.py

简单分工:

main.py
  -> 创建 FastAPI 应用,挂载路由、中间件

routers/
  -> 定义接口路径、参数、响应状态码

schemas/
  -> 定义 Pydantic 请求体、响应体

services/
  -> 写业务逻辑

dependencies/
  -> 放登录用户、数据库连接、权限等公共依赖

十四、核心语法速记

场景写法
创建应用app = FastAPI()
定义 GET 接口@app.get("/users")
路径参数@app.get("/users/{user_id}") + user_id: int
查询参数page: int = 1
查询参数校验Annotated[int, Query(ge=1)]
路径参数校验Annotated[int, Path(ge=1)]
请求体data: CreateUserRequest
模型字段校验Field(min_length=2)
响应模型response_model=UserResponse
错误响应raise HTTPException(...)
依赖注入Annotated[User, Depends(get_user)]
拆分路由APIRouter() + app.include_router()

总结

学习 FastAPI 核心语法时,不要只背装饰器。

真正要掌握的是这条链路:

类型标注
  -> 参数解析
  -> Pydantic 校验
  -> 自动文档
  -> JSON 响应

只要这条链路通了,路径参数、查询参数、请求体、响应模型、依赖注入都会变成同一套思路。