fastapi 快速上手教程
2025年11月29日大约 8 分钟约 2423 字
FastAPI教程
由于官方没有SQLModel异步文档,FastAPI的生态还无法很好稳定的使用。
FastAPI官方文档:FastAPI
FastAPI项目模板:full-stack-fastapi-template GitHub
awesome-fastapi: awesome-fastapi
FastAPI规范:fastapi-best-practices
fastapi脚手架
Pydantic: 基于python类型提示的序列化器(内部依赖)Starlette: 轻量级ASGI框架,构建高性能Asyncio服务(内部依赖)SQLModel: 数据库模型PyJWT: 认证Passlib + bcrypt: 密码加密Alembic: 数据库迁移Pytest: 测试RuffMyPy: 代码质量Uvicorn: ASGI服务器
创建中型项目结构
shell
# 生成fastapi项目
git clone https://github.com/fastapi/full-stack-fastapi-template.git temp-repo
cp -r temp-repo/backend ./demo-fastapi
rm -rf temp_repo
cd demo-fastapi
uv sync --python 3.12
.\.venv\Scripts\activate.ps1 # Windows激活虚拟环境
source .venv/Scripts/activate # Linux激活虚拟环境
# 启动项目
fastapi run --reload app/main.py创建小型项目结构
shell
# 在当前目录创建一个django项目
uv init demo-fastapi --python 3.12
cd demo-fastapi
uv add fastapi sqlmodel uvicorn
.\.venv\Scripts\activate.ps1 # Windows激活虚拟环境
source .venv/Scripts/activate # Linux激活虚拟环境models.py
"""
文档:https://fastapi.tiangolo.com/zh/tutorial/sql-databases
"""
from typing import Annotated
from fastapi import Depends
from sqlmodel import Field, Session, SQLModel, create_engine, select
# 用户模型
class UserModel(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
username: str = Field(index=True)
password: str = Field()
name: str | None = Field(index=True, default=None)
# 创建引擎
sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"
connect_args = {"check_same_thread": False}
engine = create_engine(sqlite_url, connect_args=connect_args)
def get_session():
with Session(engine) as session:
yield session
SessionDep = Annotated[Session, Depends(get_session)]
# 运行该文件时创建表
if __name__ == "__main__":
SQLModel.metadata.create_all(engine)main.py
from fastapi import FastAPI, HTTPException, Query
from typing import Annotated
from sqlmodel import select
from models import SessionDep, UserModel
from fastapi.responses import HTMLResponse
from pathlib import Path
app = FastAPI()
@app.get("/users", response_class=HTMLResponse)
async def read_root():
"""返回用户管理页面"""
html_file = Path("templates/user.html")
if html_file.exists():
return HTMLResponse(content=html_file.read_text(encoding="utf-8"))
return HTMLResponse(content="<h1>HTML 文件未找到</h1>", status_code=404)
# 用户crud
@app.get("/api/users")
async def get_users(session: SessionDep, limit: Annotated[int, Query(le=100)] = 100, offset: int = 0) -> list[UserModel]:
users = session.exec(select(UserModel).offset(offset).limit(limit)).all()
return users
@app.get("/api/users/{user_id}")
async def get_user(user_id: int, session: SessionDep) -> UserModel:
user = session.get(UserModel, user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
@app.post("/api/users")
async def create_user(user: UserModel, session: SessionDep) -> UserModel:
session.add(user)
session.commit()
session.refresh(user)
return user
@app.patch("/api/users/{user_id}")
async def patch_user(user_id: int, user: UserModel, session: SessionDep):
user_db = session.get(UserModel, user_id)
if not user_db:
raise HTTPException(status_code=404, detail="User not found")
user_data = user.model_dump(exclude_unset=True)
# 更新字段
for key, value in user_data.items():
if key != "id": # 跳过 id 字段
setattr(user_db, key, value)
session.add(user_db)
session.commit()
session.refresh(user_db)
return user_db
@app.delete("/api/users/{user_id}")
async def delete_user(user_id: int, session: SessionDep):
user = session.get(UserModel, user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
session.delete(user)
session.commit()
return {"ok": True}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=8000)templates/user.html
<!DOCTYPE html>
<html lang="zh-CN" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户管理</title>
<!-- Tailwind CSS + DaisyUI -->
<script>
tailwind.config = {
daisyui: {
themes: ["light", "dark"],
},
}
</script>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.11.1/dist/full.min.css" rel="stylesheet" type="text/css" />
<!-- Alpine.js -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.13.5/dist/cdn.min.js"></script>
<style>
.user-card { transition: all 0.2s; }
.user-card:hover { transform: translateY(-2px); }
[x-cloak] { display: none !important; }
</style>
</head>
<body class="bg-base-200 min-h-screen" x-data="userList()" x-init="loadUsers()" x-cloak>
<div class="container mx-auto p-4 max-w-7xl">
<div class="flex justify-between items-center mb-6">
<h1 class="text-4xl font-bold flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
</svg>
用户管理
</h1>
<button class="btn btn-primary" @click="openAddModal()">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
添加用户
</button>
</div>
<!-- 加载状态 -->
<div x-show="loading" class="flex justify-center items-center py-12">
<span class="loading loading-spinner loading-lg text-primary"></span>
</div>
<!-- 错误提示 -->
<div x-show="error" x-cloak class="alert alert-error mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span><strong>错误:</strong><span x-text="error"></span></span>
<button class="btn btn-sm btn-ghost" @click="error = null">✕</button>
</div>
<!-- 用户列表 -->
<div x-show="!loading && !error" x-cloak>
<div x-show="users.length === 0" class="alert alert-info">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<div>
<h3 class="font-bold">暂无用户</h3>
<div class="text-xs">点击右上角的"添加用户"按钮来创建第一个用户。</div>
</div>
</div>
<div x-show="users.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<template x-for="user in users" :key="user.id">
<div class="card bg-base-100 shadow-xl user-card">
<div class="card-body">
<h2 class="card-title">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
</svg>
<span x-text="user.username"></span>
<span x-show="user.name" class="text-base-content/60 text-sm font-normal" x-text="`(${user.name})`"></span>
</h2>
<p class="text-sm text-base-content/60" x-text="`ID: ${user.id}`"></p>
</div>
<div class="card-actions justify-end p-4 pt-0">
<div class="flex flex-col gap-2 w-full">
<button type="button"
class="btn btn-info btn-sm btn-outline w-full"
@click="viewUserDetail(user)">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
查看详情
</button>
<div class="btn-group w-full">
<button type="button"
class="btn btn-primary btn-sm flex-1"
@click="editUser(user)">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125" />
</svg>
编辑
</button>
<button type="button"
class="btn btn-error btn-sm flex-1"
@click="deleteUser(user.id)">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
</svg>
删除
</button>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
</div>
<!-- 查看详情模态框 -->
<div x-show="showDetailModal"
x-cloak
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"
@click.self="showDetailModal = false"
style="display: none;">
<div class="modal-box max-w-2xl w-full">
<h3 class="font-bold text-lg mb-4 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
</svg>
用户详情
</h3>
<div x-show="currentUser" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="label">
<span class="label-text font-semibold">用户 ID</span>
</label>
<div class="badge badge-secondary badge-lg" x-text="currentUser?.id"></div>
</div>
<div>
<label class="label">
<span class="label-text font-semibold">用户名</span>
</label>
<div class="text-lg font-medium" x-text="currentUser?.username"></div>
</div>
<div class="col-span-2">
<label class="label">
<span class="label-text font-semibold">姓名</span>
</label>
<div class="text-base" x-text="currentUser?.name || '未设置'"></div>
</div>
<div class="col-span-2">
<label class="label">
<span class="label-text font-semibold">密码</span>
</label>
<div class="text-base text-base-content/60">••••••••(已隐藏)</div>
</div>
</div>
</div>
<div class="modal-action">
<button class="btn" @click="showDetailModal = false">关闭</button>
<button class="btn btn-primary"
@click="editUser(currentUser); showDetailModal = false; showFormModal = true;">
编辑用户
</button>
</div>
</div>
</div>
<!-- 用户表单模态框(添加/编辑) -->
<div x-show="showFormModal"
x-cloak
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"
@click.self="closeFormModal()"
style="display: none;">
<div class="modal-box">
<h3 class="font-bold text-lg mb-4" x-text="form.id ? '编辑用户' : '添加用户'"></h3>
<form @submit.prevent="submitForm()">
<input type="hidden" x-model="form.id">
<div class="form-control w-full mb-4">
<label class="label">
<span class="label-text">用户名 <span class="text-error">*</span></span>
</label>
<input type="text"
class="input input-bordered w-full"
x-model="form.username"
required
placeholder="请输入用户名">
</div>
<div class="form-control w-full mb-4">
<label class="label">
<span class="label-text">密码 <span class="text-error">*</span></span>
</label>
<input type="password"
class="input input-bordered w-full"
x-model="form.password"
:required="!form.id"
placeholder="请输入密码">
<label class="label" x-show="form.id">
<span class="label-text-alt text-base-content/60">留空则不修改密码</span>
</label>
</div>
<div class="form-control w-full mb-6">
<label class="label">
<span class="label-text">姓名</span>
</label>
<input type="text"
class="input input-bordered w-full"
x-model="form.name"
placeholder="请输入姓名(可选)">
</div>
<div class="modal-action">
<button type="button" class="btn" @click="closeFormModal()">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
<script>
function userList() {
return {
users: [],
loading: true,
error: null,
form: { id: null, username: '', password: '', name: '' },
currentUser: null,
showDetailModal: false,
showFormModal: false,
async loadUsers() {
this.loading = true;
this.error = null;
try {
const res = await fetch('/api/users');
if (!res.ok) throw new Error('获取用户列表失败');
this.users = await res.json();
} catch (e) {
this.error = e.message;
} finally {
this.loading = false;
}
},
resetForm() {
this.form = { id: null, username: '', password: '', name: '' };
},
openAddModal() {
this.resetForm();
this.showFormModal = true;
},
viewUserDetail(user) {
this.currentUser = user;
this.showDetailModal = true;
},
editUser(user) {
this.form = {
id: user.id,
username: user.username,
password: '',
name: user.name || ''
};
this.showFormModal = true;
},
closeFormModal() {
this.showFormModal = false;
this.resetForm();
},
async deleteUser(id) {
if (!confirm('确定要删除这个用户吗?')) return;
try {
const res = await fetch(`/api/users/${id}`, { method: 'DELETE' });
if (!res.ok) throw new Error('删除失败');
await this.loadUsers();
} catch (e) {
this.error = e.message;
}
},
async submitForm() {
this.error = null;
try {
const url = this.form.id ? `/api/users/${this.form.id}` : '/api/users';
const method = this.form.id ? 'PATCH' : 'POST';
const body = { username: this.form.username, name: this.form.name || null };
if (!this.form.id || this.form.password) body.password = this.form.password;
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.detail || '操作失败');
}
// 关闭模态框并刷新列表
this.closeFormModal();
await this.loadUsers();
} catch (e) {
this.error = e.message;
}
}
}
}
</script>
</body>
</html>启动:
uvicorn main:app --reload
查看文档:127.0.0.1:8000/docs、127.0.0.1:8000/redoc