ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python全栈开发实战:FastAPI+SQLite构建前后端完整项目

2026/10/7 11:27:03 拓冰建站 浏览量
Python全栈开发实战:FastAPI+SQLite构建前后端完整项目 1. 项目概述1.1 第6章的核心定位从会写脚本到能搭系统先直接说结论这一章讲的是Python全栈开发中最关键的前后端打通环节。前面几章我们学了Python语法、数据结构、文件操作、函数与模块这些都是单机脚本时代的东西。但从第6章开始我们要进入Web时代——让用户通过浏览器访问你的程序让数据存进数据库让前端页面和后端接口真正跑在一个项目里。这一章面向的读者很明确已经能熟练写Python脚本、但还没完整做过一个Web项目的开发者。你不需要有前端基础我用的都是最朴素的HTMLJavaScript你也不需要懂运维部署部分我会讲最简单可靠的方案。你只需要跟着一步步做最后能跑起来一个前端页面-后端接口-数据库存取完整闭环的小项目。很多人在这个阶段会卡住原因不是Python学得不够好而是全栈涉及的技术栈一下子变宽了。前端、后端、数据库、HTTP协议、跨域、部署……每个点单独看都不难但串在一起就容易懵。第6章的价值就是把这些散落的点串成一条线用一个小项目串完整个链路。我选择的教学项目是一个灵感速记板灵感备忘录用户在网页上输入一段文字和标签提交后存入数据库页面下方实时展示所有历史记录支持按标签筛选支持删除。麻雀虽小五脏俱全——增删查、前后端交互、数据库操作全都有了。1.2 学习路径与前置要求这一章内容比较密我建议按以下路径推进不要跳着看先把虚拟环境和项目结构搭好这是后面所有操作的基础用FastAPI写最简单的GET接口确认环境能跑通接入SQLite数据库完成数据存取写前端页面通过fetch调用后端接口联调解决跨域、编码、数据格式问题部署到服务器交给真实浏览器访问前置要求不高Python 3.9以上版本能正常使用会pip install装第三方库了解基本的函数和类。如果这些还有问题建议先把前几章复习一下再回来。1.3 能解决什么问题学完这一章你能做到三件之前做不到的事第一能把Python程序暴露成HTTP接口。这意味着别人可以通过浏览器、手机App、甚至其他程序来调用你写的功能而不是只能在本地命令行里跑。第二能让数据真正存下来。脚本运行完数据就丢了这是单机脚本最大的痛点。接了数据库之后你的应用就有了记忆重启不丢数据还能查询、统计、筛选。第三能理解一个Web应用从请求到响应的完整链路。这不仅是技术能力更是工程思维的开始——你会开始考虑接口怎么设计更合理、数据校验放在哪一层、部署到服务器后怎么排查问题。2. 全栈项目设计与技术选型2.1 为什么选FastAPI而不是Flask或Django后端框架三选一我最终选了FastAPI理由非常务实Flask最轻量但很多功能要自己拼装表单解析、参数校验、自动文档都没有现成的。Django功能最全但学习曲线陡ORM、Admin、中间件这些概念对一个初学者来说太重了。FastAPI正好在中间——写起来像Flask一样简单但自带数据校验和自动生成接口文档性能还很高。而且从实际工作角度看FastAPI是目前新项目里用得越来越多的选择。它的类型提示校验机制、异步支持、自动Swagger文档在中小型项目和快速原型阶段非常高效。与其学一个经典但偏老的框架不如直接学一个现在和未来都在用的框架。对于一台个人开发机甚至小服务器来说FastAPI的同步模式已经完全够用。你不需要在刚开始就纠结性能问题先把开发体验和代码可读性提上来更重要。2.2 技术栈选型对比与取舍下面是这一章用到的完整技术栈每一项我都标明选它的理由技术项选型选型理由后端框架FastAPI轻量、自带校验和文档、支持异步、新项目主流数据库SQLite单文件、零配置、适合教学和中小项目ORMSQLAlchemyFastAPI官方推荐既能写原生SQL也能用ORM前端原生HTMLJavaScript不引入框架聚焦HTTP交互本身部署Uvicorn Nginx简单可靠资源占用小开发工具VS Code Pylance类型提示支持好国产环境友好这个组合的原则是每个环节只学最必要的东西。前端不学React/Vue因为这一章的重点是后端和HTTP交互ORM用SQLAlchemy是为了后面接MySQL时不用换技术栈部署用Uvicorn是因为FastAPI官方默认就是它性能也足够。有人可能会问为什么不直接用SQLite的sqlite3内置模块我的回答是sqlite3模块做简单读写可以但字段多了之后代码会变得很啰嗦而且后面如果要平滑迁移到MySQL会比较痛苦。SQLAlchemy的ORM方式能把表结构抽象成Python类写起来舒服迁移也容易。2.3 工程项目结构设计项目结构直接决定后续开发的体验。第6章的代码我建议严格按照下面的目录组织inspiration_board/ ├── backend/ │ ├── main.py # FastAPI应用入口 │ ├── models.py # SQLAlchemy数据模型 │ ├── database.py # 数据库连接与Session管理 │ └── schemas.py # Pydantic数据校验模型 ├── frontend/ │ ├── index.html # 前端页面 │ ├── style.css # 页面样式 │ └── app.js # 前端逻辑fetch调用接口 ├── requirements.txt # 依赖清单 └── run.sh # 一键启动脚本前后端分离目录这是最基本也是最重要的设计习惯。虽然这个项目最终是通过Nginx把前后端一起服务的但目录上必须分清楚。这样做的好处是以后前端要换成Vue项目后端接口完全不用动后端要换部署方式前端也不受影响。main.py只放路由和业务逻辑models.py只放表结构定义schemas.py只放请求和响应的数据模型。这个分层的意义在于每个文件职责单一出了问题知道去哪找改起来不会牵一发动全身。2.4 环境配置的实操细节环境配置是这一章第一个容易踩坑的地方。很多初学者习惯pip install直接装到全局环境这是大忌。不同项目依赖的版本可能冲突比如项目A需要FastAPI 0.95项目B需要FastAPI 0.100全装全局就乱了。我的习惯是先用python -m venv venv创建虚拟环境然后激活它再装依赖。Windows下激活命令是venv\Scripts\activateLinux服务器上是source venv/bin/activate。激活成功后命令行前缀会带(venv)一眼就能确认环境状态。依赖清单用requirements.txt管理写清楚版本号fastapi0.115.6 uvicorn[standard]0.34.0 sqlalchemy2.0.36 pydantic2.10.4 python-multipart0.0.20装依赖就一句话pip install -r requirements.txt。这里特别提醒python-multipart一定要装不然后面接收表单数据时会报Form data requires python-multipart to be installed这个报错很常见。3. 后端核心实现详解3.1 数据库连接与表结构定义数据库方面用SQLite文件就落在项目目录下不需要安装任何数据库服务。这是初学者最友好的方案——你的数据库服务器就是一个文件不会因为权限问题、端口冲突、服务没启动这些乱七八糟的原因报错。database.py的核心是创建一个engine和一个SessionLocalfrom sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, declarative_base SQLALCHEMY_DATABASE_URL sqlite:///./inspiration.db engine create_engine( SQLALCHEMY_DATABASE_URL, connect_args{check_same_thread: False} ) SessionLocal sessionmaker(autocommitFalse, autoflushFalse, bindengine) Base declarative_base()check_same_thread: False这个参数很多人不知道为什么加我来解释一下FastAPI的多个请求可能在不同的线程中执行而SQLite默认只允许创建它的线程访问数据库连接。如果不加这个参数就会出现随机性的RuntimeError: SQLite objects created in a thread can only be used in that same thread报错。表结构定义在models.py里用SQLAlchemy的声明式Base类来写from datetime import datetime from sqlalchemy import Column, Integer, String, Text, DateTime from database import Base class Inspiration(Base): __tablename__ inspirations id Column(Integer, primary_keyTrue, indexTrue) content Column(Text, nullableFalse) # 灵感内容 tag Column(String(50), default默认) # 标签 created_at Column(DateTime, defaultdatetime.now) # 创建时间三个字段完全对应产品的需求记内容、打标签、显示时间。nullableFalse表示内容不能为空这是数据层面的第一道防线。defaultdatetime.now注意是传函数本身而不是调用结果这样每条记录插入时都会执行一次取当前时间。初始化数据库表的代码写在main.py里from database import Base, engine import models Base.metadata.create_all(bindengine)3.2 接口设计的三个黄金原则写接口不是把数据返回给前端就行设计得合理后面联调会非常省心。我有三条原则第一接口路径按资源命名不要按操作命名。创建灵感是POST /api/inspirations获取列表是GET /api/inspirations删除是DELETE /api/inspirations/{id}。这是RESTful风格好处是路径一看就知道在操作什么资源前端人员不用猜。第二响应格式统一。所有接口返回{code: 0, message: success, data: ...}这样的固定结构。code0表示成功非0表示业务错误。这样前端只需要写一次错误处理逻辑后端加错误也方便。第三参数必须做校验。FastAPI的Pydantic模型就是干这个的在schemas.py里定义请求体from pydantic import BaseModel, Field class InspirationCreate(BaseModel): content: str Field(..., min_length1, max_length500) tag: str Field(default默认, max_length50) class InspirationResponse(BaseModel): id: int content: str tag: str created_at: datetime class Config: from_attributes TrueField(..., min_length1)表示content必填且至少1个字符前端传空字符串会被直接拦截不会走到数据库层。这就是分层防御的价值前端有前端的校验后端有后端的校验任何一层都不能省。3.3 GET与POST接口的完整实现写接口时先写一个数据库会话依赖每个请求进来都创建一个独立的会话用完自动关闭这样不会出现连接泄漏from fastapi import Depends, FastAPI, HTTPException from sqlalchemy.orm import Session import models, schemas from database import SessionLocal, engine, Base app FastAPI() Base.metadata.create_all(bindengine) def get_db(): db SessionLocal() try: yield db finally: db.close()创建新记录的POST接口from fastapi.middleware.cors import CORSMiddleware app.post(/api/inspirations, response_modelschemas.InspirationResponse) def create_inspiration(item: schemas.InspirationCreate, db: Session Depends(get_db)): db_item models.Inspiration(contentitem.content, tagitem.tag) db.add(db_item) db.commit() db.refresh(db_item) return db_item这段代码每行都有讲究。db.add把对象加入Session但此时还没写入数据库db.commit才是真正执行INSERT同时触发事务提交db.refresh是重新从数据库读取该行目的是拿到数据库自动生成的时间字段。查询列表用的是order_by排序按创建时间倒序最新的记录排最上面app.get(/api/inspirations, response_modellist[schemas.InspirationResponse]) def list_inspirations(tag: str | None None, db: Session Depends(get_db)): query db.query(models.Inspiration) if tag: query query.filter(models.Inspiration.tag tag) return query.order_by(models.Inspiration.created_at.desc()).all()tag参数是可选的前端传了就按标签过滤不传就返回全部。这个可选筛选的逻辑很常用以后你会经常见到。删除接口也要写app.delete(/api/inspirations/{item_id}) def delete_inspiration(item_id: int, db: Session Depends(get_db)): db_item db.query(models.Inspiration).filter(models.Inspiration.id item_id).first() if not db_item: raise HTTPException(status_code404, detail记录不存在) db.delete(db_item) db.commit() return {code: 0, message: deleted}注意这里先查后删查不到就返回404。直接盲删虽然能省一行代码但用户删一个不存在的ID时不会得到任何反馈前端也就没法给出友好提示。3.4 CORS跨域配置的正确处理前后端联调时最常见的拦路虎就是CORS。我用一个真实的报错场景来展开前端页面跑在http://localhost:8000第一次把静态页面放在主服务里后端接口在同一个服务里其实不会有跨域问题但如果你用VS Code的Live Server以http://127.0.0.1:5500打开前端页面再去http://localhost:8000请求接口浏览器就会报错。Access to fetch at http://localhost:8000/api/inspirations from origin http://127.0.0.1:5500 has been blocked by CORS policy这个报错的意思很直白浏览器发现发起请求的页面和服务提供接口的服务器不同源出于安全策略阻止了请求。解决办法是在后端加CORS中间件显式允许前端来源app.add_middleware( CORSMiddleware, allow_origins[http://127.0.0.1:5500], allow_credentialsTrue, allow_methods[*], allow_headers[*], )allow_origins列的是允许跨域访问你接口的前端来源。生产环境建议写具体域名不要写*通配因为通配意味着任何网站都能调用你的接口。部署阶段前后端都放在同一个域名下跨域问题就不存在了。到那时这段CORS配置甚至可以移除留着影响也不大。但开发阶段没有它联调根本无法进行。4. 前端页面与联调实战4.1 原生前端的最小可用设计前端部分我刻意没有用React或Vue因为本章的核心是让读者理解HTTP交互而不是再引入一层抽象。原生HTMLJavaScript写出来的页面虽然朴素但结构一目了然一个表单负责提交一个列表负责展示。index.html的核心结构div classcontainer h1灵感速记板/h1 form idaddForm input typetext idcontentInput placeholder记录你的灵感... required input typetext idtagInput placeholder标签可选 value默认 button typesubmit提交/button /form div idtagFilter button classtag-btn active>async function loadInspirations() { const res await fetch(/api/inspirations); const data await res.json(); renderList(data); }注意这里我用的路径是/api/inspirations不是http://localhost:8000/api/inspirations。同源下用相对路径最省心换域名、换端口都不用改代码。提交表单时设置请求方法和请求体document.getElementById(addForm).addEventListener(submit, async (e) { e.preventDefault(); const content document.getElementById(contentInput).value.trim(); const tag document.getElementById(tagInput).value.trim() || 默认; const res await fetch(/api/inspirations, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ content, tag }) }); if (res.ok) { document.getElementById(contentInput).value ; loadInspirations(); } else { alert(提交失败请检查内容是否为空); } });e.preventDefault()至关重要它阻止了表单的原生提交行为页面刷新。不写这一行点击提交后页面会跳转你的SPA体验就全毁了。Content-Type: application/json告诉后端请求体是JSON格式FastAPI的Pydantic才能正确解析。删除操作要传给后端一个ID我这里用>document.getElementById(list).addEventListener(click, async (e) { if (!e.target.classList.contains(delete-btn)) return; const id e.target.dataset.id; await fetch(/api/inspirations/${id}, { method: DELETE }); loadInspirations(); });这是事件委托的写法监听列表容器而不是每个删除按钮。好处是新增的记录不需要单独绑事件因为事件会冒泡到容器上触发同一个监听器。4.3 联调时的编码与格式排查联调阶段的报错最有代表性我列三个最常见的第一个是中文乱码。页面提交中文内容数据库里存的是乱码感想这种。这类问题的根源多数不在Python代码而在数据库连接串或HTTP响应头。SQLite存储UTF-8没有问题但响应返回时要确保FastAPI以UTF-8编码输出。实际上FastAPI默认就是UTF-8真遇到乱码优先检查HTML的meta charsetUTF-8是否写在了head里。第二个是422 Unprocessable Entity。这个状态码的意思是校验失败。前端提交的数据格式和后端Pydantic模型不匹配。最常见的原因是前端用FormData提交而后端期望JSON或者content字段传的是一个数字而模型期望字符串。第三个是删除了某条记录后再次提交发现ID没有复位。SQLite自增字段如果删到最大ID再插入就会从MAX1继续这没问题。但如果删掉了中间的记录下一次插入的ID可能不是连续的。这些都是数据库自动处理的不要试图自己控制自增ID纯属自找麻烦。4.4 页面与接口交互的调试技巧联调阶段用好浏览器开发者工具比任何IDE都管用。打开浏览器按F12进Network面板再操作页面就能看到每个请求的详细情况。看什么呢第一是状态码。2xx表示成功4xx表示客户端出问题5xx表示服务器出问题。第二是请求负载Payload看提交的JSON是不是你期望的内容。第三是响应体Response看后端返回的数据结构。这三个视图配合起来前后端谁的问题一目了然。我这里分享一个个人的调试习惯先确认前端发送的请求格式完全正确再去怀疑后端。因为前端是你能完全控制的部分前端确认无误了后端的勾子就有明确方向。我经手过的联调问题80%出在前端的Content-Type设置不对、JSON序列化格式不对或者请求路径写错后端其实一直都没问题。5. 部署上线与常见问题排查5.1 Uvicorn部署与静态文件服务开发阶段可以用uvicorn backend.main:app --reload直接跑但生产环境要换一种方式。--reload是开发用的热重载生产必须去掉。把服务暴露到公网要用0.0.0.0作为监听地址uvicorn backend.main:app --host 0.0.0.0 --port 8000我在main.py里把前端静态文件也交给了FastAPI托管这样只有一个服务端就够了部署起来最简单。做法是挂载StaticFilesfrom fastapi.staticfiles import StaticFiles from pathlib import Path BASE_DIR Path(__file__).resolve().parent.parent app.mount(/static, StaticFiles(directoryBASE_DIR / frontend), namestatic)根路径返回前端页面from fastapi.responses import FileResponse app.get(/) def read_index(): return FileResponse(BASE_DIR / frontend / index.html)这样整个应用就是一个8000端口的大服务根路径指向页面/api/*指向接口/static/*指向样式和脚本。避免了Nginx配置静态目录和代理的复杂性对中小型项目非常够用。5.2 用进程守护确保服务稳定直接跑uvicorn命令的问题是终端一关服务就停了。要让服务一直跑常见方案有nohup、systemd、supervisor、Docker等。初学者最实用的是nohup一行命令就能实现后台常驻nohup uvicorn backend.main:app --host 0.0.0.0 --port 8000 app.log 21 app.log把stdout写入app.log21把stderr也重定向到同一文件使其后台运行。这样服务不会被终端关闭杀掉所有日志都在app.log里。不过nohup有局限服务器重启后不会自动拉起。如果你有更高要求建议用systemd写一个服务单元文件管理。但那是另一个话题本章先掌握nohup这个最朴素的方案。5.3 上线后常见问题的排查思路我把上线后用户反馈最多的几个问题及排查方向整理成了一张速查表现象可能原因排查方式页面打不开502 Bad Gateway后端服务没起检查ps aux | grep uvicorn有没有进程页面能开提交按钮没反应JS报错或有接口超时F12看Console和Network面板接口报500内部错误代码异常但被吞了查看app.log里的Traceback数据库数据丢失SQLite文件路径不对检查inspiration.db是否在项目目录中文显示为问号终端编码问题在代码里统一使用UTF-8这里最容易被忽视的是路径问题。用Path(__file__).resolve()拿到的是代码文件所在目录再用parent去找项目根目录不管你在哪个目录执行启动命令路径都不会错。千万不要用相对路径frontend因为相对路径依赖当前工作目录换个地方执行命令就会找不到文件。5.4 服务重启与数据备份的实践经验最后分享两个实际运维层面的经验项目要想长期稳定运行这两件事必须做。第一是更新代码后的重启流程。我的一般操作是先git pull拉最新代码然后执行pkill -f uvicorn杀掉旧进程再用一条新的nohup命令启动。先杀再启的顺序不能反否则新代码不会生效。如果你改了表结构models.py记得先删掉旧的数据库文件或写迁移脚本否则启动时会因为缺少字段而报错。第二是数据库备份。SQLite就是一个文件备份就是拷贝文件简单高效。我写了一句定时备份cp inspiration.db backup/inspiration_$(date %Y%m%d_%H%M%S).db配合cron每天执行一次数据就有兜底了。别看这方法简单粗暴对小项目来说比任何复杂的备份方案都实用。6. 进阶方向与个人经验补充6.1 本章项目还能怎么扩展这个灵感速记板做通了之后扩展方向非常多而且每一个方向都能让你学到新东西第一个方向是给记录加上编辑功能。前端加一个编辑按钮后端加一个PUT /api/inspirations/{id}接口。这里会接触到HTTP的PUT/PATCH方法以及部分字段更新的技巧比如怎么只更新传入的字段而不覆盖其他字段。第二个方向是加一个搜索框支持按关键词模糊搜索。SQLAlchemy的filter(models.Inspiration.content.contains(keyword))就能实现LIKE查询很简单但能显著提升产品的实用度。第三个方向是加计数统计。比如按标签统计数量、按天统计新增条数这是往数据分析和可视化方向走的第一步。配合ECharts这样的图表库就能画出趋势图。第四个方向是把SQLite替换成MySQL。SQLAlchemy的好处在这里体现出来——连接串从sqlite:///./inspiration.db换成mysqlpymysql://user:passwordlocalhost/inspiration大部分代码不用动。这一步做完你对ORM的数据库无关性就有切身体会了。6.2 几个值得长期保持的开发习惯最后分享几条我在这个项目里反复用到的经验它们不只在写Python时有用做任何后端项目都适用。第一接口返回值永远走统一格式。哪怕是报错也要返回{code: 400, message: 具体错误信息}这种结构化格式不要只丢一个HTTP状态码就完事。前端拿到结构化错误信息能直接展示给用户看而不是显示一行Internal Server Error。第二每写完一个接口先用浏览器或Postman测一次再往前端走。在接口层面确认没问题了再去做前端联调排查范围一下就缩小了。FastAPI自带的Swagger文档页面访问/docs就是最好的测试工具点几下就能发请求。第三代码里所有输入都要考虑如果用户传了空字符串怎么办如果传了超长文本怎么办。这些边界情况不是你故意刁难用户而是真实场景中一定会出现的东西。Pydantic的Field约束、前端required属性都是在边界防线上加砖。第四也是最重要的一条不要试图一次把代码写到完美。先跑通流程再逐步优化这是软件工程最朴素也最有效的策略。我在这个项目里也是先写一个巨简单的版本不校验、不分类、不能删然后一点点加上去。每次只改一个功能点出问题能立刻定位到具体是哪一步改坏了。这一章的内容就到这里。你可以一边看一边敲代码跑通了再回头对照排查表看看自己有没有踩过类似的坑。全栈开发不是一步登天的技能但能跑通第一个前后端打通的完整项目后面很多概念学起来就会快得多。