一、前言

这篇文章记录一个基于 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_avalue_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.pydata_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_avalue_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.pydatabase.pydata_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. 增加前端组件化

目前这个阶段最重要的不是功能有多复杂,而是先把:

数据库 → 后端接口 → 前端页面

这条基础链路稳定跑通。

更多推荐