FastAPI + MySQL + Vue 实现一个简单数据看板:全栈链路实现
一、前言
这篇文章记录一个基于 FastAPI + MySQL + Vue 的前后端分离小项目。
项目目标比较简单:后端从 MySQL 数据库中读取数据,通过 FastAPI 提供接口,再由 Vue 前端请求接口并展示数据。
整体流程如下:
MySQL 数据库
↓
FastAPI 后端接口
↓
Vue 前端页面
本篇文章主要完成以下内容:
1. 使用 FastAPI 搭建后端接口
2. 使用 MySQL 保存数据
3. 使用 python-dotenv 管理数据库配置
4. 将数据库连接和数据操作从 main.py 中拆出来
5. 提供最新数据接口和历史数据接口
6. 使用 Vue 调用后端接口并展示数据
这一版不做复杂业务逻辑,也不做登录权限,重点是先把:
数据库 → 后端接口 → 前端展示
这条基础链路跑通。
二、项目目录结构
最终项目结构如下:
vue_yugang/
│
├── backend/
│ ├── main.py
│ ├── .env
│ └── mysql_red/
│ ├── __init__.py
│ ├── database.py
│ └── data_service.py
│
└── frontend/
├── package.json
├── index.html
└── src/
└── App.vue
各文件作用如下:
backend/main.py FastAPI 主接口文件
backend/.env 数据库配置文件
mysql_red/database.py MySQL 连接配置
mysql_red/data_service.py 数据库操作函数
frontend/src/App.vue Vue 前端页面
这里我没有把所有代码都堆在 main.py 里,而是把数据库连接和数据查询拆了出去。这样项目结构会更清楚,后面继续扩展也更方便。
三、创建 MySQL 数据库和数据表
先创建一个数据库:
CREATE DATABASE IF NOT EXISTS demo_system
DEFAULT CHARACTER SET utf8mb4
DEFAULT COLLATE utf8mb4_unicode_ci;
USE demo_system;
然后创建一张简单的数据表:
CREATE TABLE IF NOT EXISTS dashboard_data (
id INT PRIMARY KEY AUTO_INCREMENT,
value_a FLOAT NOT NULL,
value_b FLOAT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
这里我用 value_a 和 value_b 代表两个普通指标值,不绑定具体业务场景。
为了方便测试,可以先插入几条数据:
INSERT INTO dashboard_data (value_a, value_b)
VALUES
(26.5, 520),
(27.1, 600),
(25.8, 480);
查看数据:
SELECT * FROM dashboard_data ORDER BY id DESC LIMIT 5;
四、后端环境配置
进入 backend 文件夹,安装依赖:
pip install fastapi uvicorn pymysql python-dotenv cryptography
也可以创建一个 requirements.txt:
fastapi
uvicorn
pymysql
python-dotenv
cryptography
然后执行:
pip install -r requirements.txt
这些依赖分别用于:
fastapi 搭建后端接口
uvicorn 启动 FastAPI 服务
pymysql 连接 MySQL 数据库
python-dotenv 读取 .env 配置文件
cryptography 解决部分 MySQL 连接认证问题
五、配置 .env
在 backend 文件夹下创建 .env 文件:
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=root
DB_PASSWORD=你的MySQL密码
DB_NAME=demo_system
这里需要把:
你的MySQL密码
改成自己的 MySQL 密码。
我当时的疑问:为什么不直接把数据库密码写在 main.py 里?
一开始我也想直接这样写:
DB_CONFIG = {
"host": "127.0.0.1",
"user": "root",
"password": "123456",
"database": "demo_system"
}
这样确实能运行,但不太安全。
因为数据库密码属于敏感信息,如果以后把代码上传到 GitHub 或者发给别人,很容易把密码一起暴露出去。
所以更合适的做法是把数据库配置写到 .env 文件里,再在 Python 代码中读取。
这样做的好处是:
1. main.py 中不会直接出现数据库密码
2. 修改数据库配置时不用改业务代码
3. 项目结构更清楚
4. 后续上传代码时可以把 .env 加入 .gitignore
六、编写数据库连接文件 database.py
文件路径:
backend/mysql_red/database.py
代码如下:
import os
import pymysql
from dotenv import load_dotenv
load_dotenv()
DB_CONFIG = {
"host": os.getenv("DB_HOST", "127.0.0.1"),
"port": int(os.getenv("DB_PORT", 3306)),
"user": os.getenv("DB_USER"),
"password": os.getenv("DB_PASSWORD"),
"database": os.getenv("DB_NAME"),
"charset": "utf8mb4"
}
def get_db_connection():
return pymysql.connect(**DB_CONFIG)
这个文件只负责一件事:
创建 MySQL 数据库连接
以后其他文件如果需要连接数据库,就直接调用:
get_db_connection()
我当时的疑问:MySQL 软件没打开,代码还能连接数据库吗?
这里需要区分两个东西:
MySQL Workbench / Navicat 没打开:没关系
MySQL Server 服务没启动:不行
FastAPI 连接的是 MySQL Server 服务,不是连接 Workbench 或 Navicat 这种图形界面软件。
也就是说:
FastAPI
↓
MySQL Server
↓
数据库
如果 MySQL Server 没有启动,后端代码就没有地方可以连接,数据库读写也会失败。
代码可以做的是:
1. 数据库连接失败时返回清楚的错误
2. 后续加入重试机制
3. 后续加入本地缓存或备份机制
但是代码不能在 MySQL Server 完全没运行的情况下,直接把数据写进 MySQL。
七、编写数据库操作文件 data_service.py
文件路径:
backend/mysql_red/data_service.py
代码如下:
from .database import get_db_connection
def read_dashboard_data(limit: int):
conn = None
try:
conn = get_db_connection()
with conn.cursor() as cursor:
sql = """
SELECT id, value_a, value_b, created_at
FROM dashboard_data
ORDER BY id DESC
LIMIT %s
"""
cursor.execute(sql, (limit,))
rows = cursor.fetchall()
data = []
for row in rows:
data.append({
"id": row[0],
"value_a": row[1],
"value_b": row[2],
"created_at": str(row[3])
})
data.reverse()
return data
finally:
if conn:
conn.close()
这个文件负责数据库操作。这里暂时只写了一个读取函数:
read_dashboard_data(limit)
它可以根据 limit 读取最新的若干条数据。
八、为什么要写 from .database import get_db_connection
我的项目结构是:
backend/
├── main.py
└── mysql_red/
├── __init__.py
├── database.py
└── data_service.py
database.py 和 data_service.py 都在 mysql_red 文件夹里。
所以在 data_service.py 里要写:
from .database import get_db_connection
这里前面的点 . 表示:
从当前包里面找 database.py
如果写成:
from database import get_db_connection
可能会报错:
ModuleNotFoundError: No module named 'database'
因为 Python 会去外层目录找 database.py,但实际文件在 mysql_red 文件夹里面。
九、为什么不把 SQL 查询都写在 main.py 里?
刚开始为了方便,我也会想把所有内容都写在 main.py:
接口
数据库连接
SQL 查询
数据处理
错误处理
但是这样写到后面会非常乱。
所以我把项目拆成了三个部分:
main.py 只写接口
database.py 只写数据库连接
data_service.py 只写数据库操作
这样结构更清楚:
前端请求接口
↓
main.py 接收请求
↓
data_service.py 查询数据库
↓
database.py 提供数据库连接
main.py 不需要关心具体 SQL 怎么写,只需要调用函数。
十、为什么最新数据和历史数据可以共用一个函数?
项目里需要两个接口:
GET /api/data/latest 获取最新一条数据
GET /api/data/history 获取历史数据
一开始很容易分别写两套 SQL。
但它们本质上都是从同一张表里读取数据,区别只是:
latest:读取 1 条
history:读取多条
所以可以共用一个函数:
read_dashboard_data(limit)
最新数据接口调用:
data = read_dashboard_data(limit=1)
历史数据接口调用:
data = read_dashboard_data(limit=limit)
这样可以减少重复代码,后面如果表结构调整,只需要改一个地方。
十一、cursor.execute() 和 fetchall() 是什么?
在 data_service.py 里有这段代码:
cursor.execute(sql, (limit,))
rows = cursor.fetchall()
我一开始也容易误解,以为 execute() 执行完 SQL 就直接拿到结果了。
实际上:
cursor.execute()
只是执行 SQL。
如果这条 SQL 有查询结果,还需要用:
fetchone()
或者:
fetchall()
把结果取出来。
区别是:
fetchone():只取一行
fetchall():取出所有行
比如:
cursor.execute("SELECT DATABASE();")
result = cursor.fetchone()
返回结果可能是:
('demo_system',)
这是一个元组。如果要取出数据库名,就写:
result[0]
在本项目中,历史数据可能有多条,所以使用:
rows = cursor.fetchall()
十二、为什么 SQL 里用 %s,而不是字符串拼接?
代码中使用的是参数化 SQL:
sql = """
SELECT id, value_a, value_b, created_at
FROM dashboard_data
ORDER BY id DESC
LIMIT %s
"""
cursor.execute(sql, (limit,))
这里的 %s 是占位符,真实参数通过:
cursor.execute(sql, (limit,))
传进去。
不建议这样写:
sql = f"SELECT * FROM dashboard_data LIMIT {limit}"
也不建议这样写:
sql = "SELECT * FROM dashboard_data LIMIT " + str(limit)
直接拼接 SQL 可读性差,也有 SQL 注入风险。参数化 SQL 更规范,也更安全。
十三、编写 FastAPI 主文件 main.py
文件路径:
backend/main.py
完整代码如下:
from fastapi import FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from mysql_red.data_service import read_dashboard_data
app = FastAPI(title="Demo Dashboard API")
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:5173",
"http://127.0.0.1:5173",
"http://localhost:8080",
"http://127.0.0.1:8080"
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
def root():
return {
"message": "Demo Dashboard API is running"
}
@app.get("/api/data/latest")
def get_latest_data():
try:
data = read_dashboard_data(limit=1)
if len(data) == 0:
return {
"message": "No data yet",
"data": None
}
return {
"message": "Latest data",
"data": data[0]
}
except Exception:
raise HTTPException(
status_code=500,
detail="Database read failed"
)
@app.get("/api/data/history")
def get_history_data(limit: int = Query(30, ge=1, le=200)):
try:
data = read_dashboard_data(limit=limit)
return {
"message": "History data",
"data": data
}
except Exception:
raise HTTPException(
status_code=500,
detail="Database read failed"
)
这个文件提供两个主要接口:
GET /api/data/latest 获取最新一条数据
GET /api/data/history 获取历史数据
十四、为什么要写 limit: int = Query(30, ge=1, le=200)
历史数据接口中有这一行:
def get_history_data(limit: int = Query(30, ge=1, le=200)):
意思是:
limit 默认是 30
limit 最小是 1
limit 最大是 200
所以可以这样请求:
/api/data/history?limit=50
但是不能请求特别大的值,比如:
/api/data/history?limit=999999
如果不限制,一次性查询太多数据,可能会拖慢数据库。
所以这里的 Query 不是复杂业务逻辑,而是一个基础接口保护。
十五、为什么要配置 CORS?
Vue 前端默认运行在:
http://localhost:5173
FastAPI 后端运行在:
http://127.0.0.1:8000
它们端口不同,所以浏览器会认为这是跨域请求。
如果 FastAPI 不允许跨域,Vue 请求后端接口时可能会失败。
所以需要在 main.py 中加入:
from fastapi.middleware.cors import CORSMiddleware
并配置:
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:5173",
"http://127.0.0.1:5173",
"http://localhost:8080",
"http://127.0.0.1:8080"
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
这里的 5173 是 Vite 默认端口,8080 是一些 Vue 项目常见端口。
十六、启动 FastAPI 后端
进入 backend 文件夹:
cd backend
启动后端:
uvicorn main:app --reload
如果启动成功,会看到类似输出:
Uvicorn running on http://127.0.0.1:8000
打开接口文档:
http://127.0.0.1:8000/docs
可以看到两个接口:
GET /api/data/latest
GET /api/data/history
测试最新数据接口,如果数据库中有数据,会返回类似:
{
"message": "Latest data",
"data": {
"id": 3,
"value_a": 25.8,
"value_b": 480,
"created_at": "2026-06-01 14:20:00"
}
}
十七、创建 Vue 前端项目
回到项目根目录:
cd ..
创建 Vue 项目:
npm create vite@latest frontend -- --template vue
这句命令的意思是:
使用 Vite 创建一个新的 Vue 项目,项目文件夹名称是 frontend
其中:
npm Node.js 的包管理工具
create vite@latest 使用最新版 Vite 创建项目
frontend 新项目文件夹名称
-- --template vue 指定模板为 Vue
然后进入前端目录:
cd frontend
安装依赖:
npm install
启动前端:
npm run dev
成功后会看到:
http://localhost:5173/
十八、编写 Vue 页面 App.vue
打开:
frontend/src/App.vue
删除原来的内容,替换成下面代码:
<template>
<div class="page">
<h1>数据看板 Demo</h1>
<button @click="loadLatestData">
刷新数据
</button>
<div class="card" v-if="loading">
正在读取数据...
</div>
<div class="card error" v-else-if="error">
{{ error }}
</div>
<div class="card" v-else-if="dashboardData">
<p>指标 A:{{ dashboardData.value_a }}</p>
<p>指标 B:{{ dashboardData.value_b }}</p>
<p>时间:{{ dashboardData.created_at }}</p>
</div>
<div class="card" v-else>
暂无数据
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue"
const dashboardData = ref(null)
const loading = ref(false)
const error = ref("")
async function loadLatestData() {
loading.value = true
error.value = ""
try {
const response = await fetch("http://127.0.0.1:8000/api/data/latest")
if (!response.ok) {
throw new Error("request failed")
}
const result = await response.json()
dashboardData.value = result.data
} catch (e) {
error.value = "无法连接后端,请确认 FastAPI 正在运行"
} finally {
loading.value = false
}
}
onMounted(() => {
loadLatestData()
})
</script>
<style scoped>
.page {
width: 500px;
margin: 80px auto;
text-align: center;
font-family: Arial, sans-serif;
}
button {
padding: 10px 18px;
border: none;
border-radius: 8px;
background: #1677ff;
color: white;
cursor: pointer;
}
.card {
margin-top: 20px;
padding: 20px;
border-radius: 12px;
background: #ffffff;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.1);
}
.error {
color: red;
}
</style>
这个页面会请求后端接口:
http://127.0.0.1:8000/api/data/latest
然后展示数据库中的最新一条记录。
十九、为什么 Vue 里要用 onMounted()
Vue 代码里有这段:
onMounted(() => {
loadLatestData()
})
意思是:
页面加载完成后,自动执行 loadLatestData()
如果没有这段代码,页面打开时不会自动读取数据,必须手动点击按钮才会请求后端接口。
加上之后,用户一打开页面,就能看到数据库中的最新记录。
二十、为什么前端请求要用 try...catch
前端请求接口时可能会失败,比如:
1. 后端没有启动
2. 数据库连接失败
3. 接口路径写错
4. 网络请求失败
所以不能只写:
const response = await fetch(...)
const result = await response.json()
更稳一点的写法是:
try {
const response = await fetch("http://127.0.0.1:8000/api/data/latest")
if (!response.ok) {
throw new Error("request failed")
}
const result = await response.json()
dashboardData.value = result.data
} catch (e) {
error.value = "无法连接后端,请确认 FastAPI 正在运行"
}
这样即使后端没有启动,页面也不会直接崩掉,而是显示错误提示。
二十一、同时运行前后端
需要打开两个终端。
终端 1:运行后端
cd backend
uvicorn main:app --reload
终端 2:运行前端
cd frontend
npm run dev
然后打开:
http://localhost:5173/
如果页面能够显示数据库中的 value_a、value_b 和时间,就说明
MySQL → FastAPI → Vue
这条链路已经跑通。
二十二、常见问题
1. ModuleNotFoundError: No module named 'database'
如果目录结构是:
backend/
├── main.py
└── mysql_red/
├── database.py
└── data_service.py
那么 data_service.py 里面应该写:
from .database import get_db_connection
不要写成:
from database import get_db_connection
2. 前端请求后端失败
先确认后端是否启动:
http://127.0.0.1:8000/docs
如果接口文档打不开,说明 FastAPI 没有运行。
如果后端已经运行,但前端还是访问失败,就检查 main.py 是否配置了 CORS。
3. 数据库连接失败
检查 MySQL Server 是否正在运行。
注意,不是打开 MySQL Workbench 或 Navicat,而是 MySQL 服务本身要运行。
Windows 可以打开:
services.msc
找到 MySQL 服务,确认状态是“正在运行”。
4. 页面显示“暂无数据”
可以先检查数据库表里是否有数据:
USE demo_system;
SELECT * FROM dashboard_data ORDER BY id DESC LIMIT 5;
如果没有数据,可以手动插入:
INSERT INTO dashboard_data (value_a, value_b)
VALUES (26.5, 520);
然后刷新前端页面。
二十三、这次项目中我踩过的几个点
这次项目虽然只是一个简单的数据看板,但做的过程中也遇到了一些容易忽略的问题。
第一,MySQL Workbench 没打开不代表数据库不能用,真正需要启动的是 MySQL Server 服务。后端连接的是数据库服务,不是图形化软件界面。
第二,数据库密码最好不要直接写在 main.py 里。开发时这样写很方便,但如果后面上传代码,很容易泄露敏感信息,所以我改成了 .env 配置。
第三,main.py 不适合放太多逻辑。刚开始为了方便,我把接口、数据库连接和查询逻辑都写在一个文件里,但很快发现重复代码比较多,也不利于后面扩展。所以最后拆成了 main.py、database.py 和 data_service.py。
第四,最新数据和历史数据其实可以共用一个查询函数。它们的差别只是 limit 不同,没有必要重复写两遍 SQL。
第五,Vue 请求 FastAPI 时需要注意跨域问题。因为前端和后端运行在不同端口,所以后端要配置 CORS。
二十四、阶段总结
到这里,一个最小版的前后端分离数据看板已经完成。
目前实现了:
1. FastAPI 连接 MySQL
2. 后端读取最新数据
3. 后端读取历史数据
4. Vue 调用后端接口
5. 前端展示数据库记录
项目整体链路是:
MySQL 数据库
↓
FastAPI 后端
↓
Vue 前端
这一版虽然功能不复杂,但结构比较清楚:
main.py 只放接口
database.py 只放数据库连接
data_service.py 只放数据库操作
.env 保存数据库配置
后续可以继续扩展:
1. 增加折线图展示历史数据
2. 增加分页查询
3. 增加搜索条件
4. 增加数据新增、修改、删除接口
5. 增加登录和权限控制
6. 增加前端组件化
目前这个阶段最重要的不是功能有多复杂,而是先把:
数据库 → 后端接口 → 前端页面
这条基础链路稳定跑通。
更多推荐
所有评论(0)