使用 Next.js + Prisma + MySQL 开发全栈项目

本文介绍如何用 Next.jsPrismaMySQL 搭建全栈应用:包含 MySQL 与 MySQL Workbench 的安装与基本使用 、创建项目、目录含义、数据库建模、Migrations(迁移) 的工作方式与命令,并附代码示例。


一、技术栈分工

技术 作用
Next.js React 框架:路由、SSR、服务端组件、Route Handlers(API)
Prisma ORM:类型安全查询、Schema 定义、迁移版本管理
MySQL 关系型数据库:表、外键、事务、索引

二、如何创建 Next.js 项目

2.1 前置条件

  • 已安装 Node.js (建议 LTS 版本)
  • 包管理器任选:npmpnpmyarn

2.2 使用官方脚手架

在项目父目录执行:

bash 复制代码
npx create-next-app@latest my-app

交互式选项说明(根据版本可能略有差异):

选项 建议 说明
TypeScript Yes 与 Prisma、大型项目更匹配
ESLint Yes 统一代码风格
Tailwind CSS 按需 UI 快速开发可选用
src/ directory 按需 若选 Yes,应用代码在 src/app
App Router Yes 当前推荐,与本文目录说明一致
Turbopack 按需 开发时可选更快的打包器

进入项目并安装依赖:

bash 复制代码
cd my-app
npm install

2.3 本地运行

bash 复制代码
npm run dev

浏览器访问 http://localhost:3000。默认端口为 3000

2.4 常用脚本(package.json

命令 作用
npm run dev 开发模式,热更新
npm run build 生产构建
npm run start 启动生产构建后的服务(需先 build
npm run lint 运行 ESLint

三、典型目录与文件说明(App Router)

以下以 未使用 src/ 的默认结构为例;若创建时选了 src/,则把 appcomponents 等放在 src/ 下即可。

bash 复制代码
my-app/
├── app/                      # App Router:页面、布局、路由
│   ├── layout.tsx            # 根布局(全局 HTML 壳、字体、Provider)
│   ├── page.tsx              # 路由 "/" 的首页
│   ├── globals.css           # 全局样式
│   ├── loading.tsx           # 可选:该路由段的加载 UI(Suspense)
│   ├── error.tsx             # 可选:该路由段的错误边界
│   └── api/                  # Route Handlers(HTTP API)
│       └── users/
│           └── route.ts      # 对应路径 /api/users
├── public/                   # 静态资源:favicon、图片等,URL 根路径直接访问
├── prisma/
│   ├── schema.prisma         # 数据模型与数据库连接配置
│   └── migrations/           # 迁移历史(由 prisma migrate 生成,勿手改 SQL 逻辑)
├── lib/                      # 常用:工具函数、Prisma 单例等(自建)
│   └── prisma.ts
├── components/               # 可复用 UI 组件(自建)
├── .env                      # 本地环境变量(勿提交密钥)
├── .env.example              # 环境变量示例(可提交)
├── next.config.ts / .js      # Next 构建与运行时配置
├── tsconfig.json             # TypeScript 配置
└── package.json

3.1 app/ 目录

  • page.tsx:该路由的页面 UI;文件夹名即 URL 路径段。
  • layout.tsx:嵌套布局,子路由共享外壳(导航栏、侧边栏等)。
  • route.ts :只处理 HTTP 方法(GET、POST 等),不渲染页面,用于 REST API
  • 默认导出为 Server Component ,可直接 async 并访问数据库;需要浏览器事件、hooks 时在文件顶部加 "use client"

3.2 public/

放不需要经过打包处理的静态文件,例如 public/logo.png 对应访问路径 /logo.png

3.3 prisma/

  • schema.prisma :定义数据源、生成器、Model(表结构)。
  • migrations/ :每次执行 prisma migrate dev(或生产用 deploy)产生的迁移记录,见下文「Migrations 详解」。

3.4 根目录配置文件

  • next.config.*:图片域名、重定向、实验特性等。
  • tsconfig.json :路径别名(如 @/*)常在这里配置,与 import 有关。

四、MySQL 安装与 MySQL Workbench

4.1 官方下载地址

以下均为 Oracle MySQL 官方站点,请从 Downloads 中选择操作系统与安装包类型。

软件 说明 下载页
MySQL Community Server 数据库服务本体(必装,供本地/服务器运行 MySQL) dev.mysql.com/downloads/m...
MySQL Installer(仅 Windows) 一站式安装器,可勾选 Server、Workbench、Shell 等 dev.mysql.com/downloads/i...
MySQL Workbench 图形化管理与 SQL 开发工具(可选但强烈推荐) dev.mysql.com/downloads/w...

说明

  • 若使用 MySQL Installer for Windows ,通常一次勾选 MySQL Server + MySQL Workbench 即可,无需单独下载 Workbench。
  • macOS 可使用官网 DMG 或包管理器(如 Homebrew:brew install mysql);Linux 常用发行版仓库或官网 APT/YUM 仓库,Workbench 可单独安装。

4.2 在 Windows 上安装 MySQL Server(常见流程)

以下以 MySQL Installer 为例(适合本机开发):

  1. 打开 MySQL Installer 下载页,选择 Windows (x86, 64-bit), MSI Installer(体积较大的完整安装包或在线安装包均可)。
  2. 运行安装程序,选择安装类型:
    • Developer Default:会安装 Server、Workbench、Shell 等,适合开发。
    • 若只需数据库服务,可选 Server only
  3. 按向导执行 Execute 安装所选组件。
  4. Type and Networking 中保持默认端口 3306 (除非端口冲突,需与 Prisma 的 DATABASE_URL 一致)。
  5. Authentication 中保持默认 Strong Password(推荐)。
  6. 设置 root 用户密码并牢记;后续 Workbench、Prisma 连接都会用到。
  7. 将 MySQL 配置为 Windows Service ,并勾选 Start the MySQL Server at System Startup(开机自启,便于开发)。
  8. 完成安装。

4.3 安装后如何确认 MySQL 已运行

  • 服务 :按 Win + R,输入 services.msc,查找 MySQLMySQL80 等服务名,状态应为 正在运行
  • 命令行(若安装时勾选了命令行客户端且已加入 PATH):
bash 复制代码
mysql --version
  • 若无法直接执行 mysql,可在「开始菜单」打开 MySQL Command Line Client,用 root 密码登录测试。

4.4 安装 MySQL Workbench

  • 若已通过 MySQL Installer 勾选 Workbench,可跳过单独安装。
  • 否则打开 Workbench 下载页,选择 Windows / macOS / Linux 对应安装包,按向导安装即可。

4.5 使用 MySQL Workbench 连接数据库

  1. 打开 MySQL Workbench
  2. 主界面 MySQL Connections 区域点击 「+」MySQL Connections 旁的加号,新建连接。
  3. 填写连接参数(本地开发常见值):
字段 典型值 说明
Connection Name 任意名称,如 Local Dev 仅显示用
Hostname 127.0.0.1localhost 本机数据库
Port 3306 与安装时一致
Username root 或你创建的其他用户
Password --- 点击 Store in Keychain / Vault... 保存密码,避免每次输入
  1. 点击 Test Connection ,提示成功即可 OK 保存。
  2. 双击该连接进入主工作区:左侧为 Navigator (库表列表),中间为 SQL 编辑与结果区

4.6 MySQL Workbench 基本操作

操作 步骤
创建数据库(Schema) 左侧 Schemas 面板空白处右键 → Create Schema... → 输入库名(如 mydb)→ Apply ;或在 Query 窗口执行:CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
执行 SQL 顶部 File → New Query Tab 或工具栏新建查询标签 → 输入 SQL → 选中语句 → 点击 闪电图标 执行(或 Ctrl + Enter)。
查看表数据 Schemas 中展开数据库 → Tables → 表名右键 → Select Rows - Limit 1000
查看表结构 表名右键 → Table InspectorAlter Table
新建用户/授权(进阶) 菜单 Server → Users and Privileges(需相应权限);开发阶段常用 root,生产环境应使用最小权限账号。

Prisma 配合时:先在 Workbench 里 创建空库 (如 mydb),再在项目 .env 中把 DATABASE_URL 指向该库;表结构通常交给 prisma migrate 管理,无需在 Workbench 里手工建每张表(除非你不用迁移、纯手写 SQL)。

五、接入 Prisma 与 MySQL

5.1 安装

bash 复制代码
npm install prisma @prisma/client
npx prisma init

prisma init 会创建 prisma/schema.prisma,并在项目根目录提示创建 .env

5.2 配置连接串(.env

env 复制代码
DATABASE_URL="mysql://用户名:密码@主机:3306/数据库名"

示例(本地 MySQL):

env 复制代码
DATABASE_URL="mysql://root:secret@localhost:3306/mydb"

注意 :将 .env 加入 .gitignore,仓库中只保留 .env.example(不含真实密码)。

5.3 schema.prisma 示例

prisma 复制代码
datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  posts     Post[]
  createdAt DateTime @default(now())
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  published Boolean  @default(false)
  authorId  Int
  author    User     @relation(fields: [authorId], references: [id])
  createdAt DateTime @default(now())
}

5.4 生成 Client

修改 schema 后执行:

bash 复制代码
npx prisma generate

会依据当前 schema.prisma 生成 node_modules/.prisma/client 等,供 TypeScript 使用。


六、Migrations(数据库迁移)详解

Migration 指:把「数据库结构应该怎样」从 Prisma Schema 出发,变成 可重复执行、可版本控制 的 SQL 变更步骤。团队协作时,所有人用同一套迁移文件,就能把本地/测试/生产数据库结构对齐。

6.1 为什么需要迁移

  • 可回顾 :每次结构变更都有对应 SQL 文件与命名(如 20240323120000_init)。
  • 可复现:新同事或新环境执行同一套迁移即可得到一致表结构。
  • 与「只改库不改文件」对比 :手动在 Navicat 里改表,无法自动同步到其他人的机器,也容易与 schema.prisma 不一致。

6.2 开发环境常用命令:prisma migrate dev

首次或每次修改 schema.prisma 后:

bash 复制代码
npx prisma migrate dev --name 描述本次变更的英文短语

例如:

bash 复制代码
npx prisma migrate dev --name init
npx prisma migrate dev --name add_post_published_index

该命令会:

  1. 对比当前 schema 与数据库状态,生成 新的 SQL 迁移文件到 prisma/migrations/<时间戳>_<name>/migration.sql
  2. 应用 这些迁移到当前 DATABASE_URL 指向的数据库
  3. 触发 prisma generate,更新 Prisma Client。

适合:本地开发共享开发数据库 (团队约定好同一条 DATABASE_URL 时要注意冲突)。

6.3 生产 / CI:prisma migrate deploy

生产构建流程 或服务器上,不要migrate dev(它会交互式、且偏向开发工作流)。应使用:

bash 复制代码
npx prisma migrate deploy

作用 :只执行 prisma/migrations尚未应用 的迁移,根据当前数据库反向改 schema,适合流水线与只读权限受限的环境。

典型顺序:

  1. 构建应用:npm run build
  2. 部署前或启动前:npx prisma migrate deploy
  3. 启动:npm run start

6.4 db pushmigrate 的区别

命令 适用场景
prisma migrate dev 有迁移历史、团队需要版本化 SQL;推荐正式项目
prisma db push 快速把 schema 推到 数据库,生成迁移文件;原型、个人玩具项目或明确不保留迁移历史时可用

生产环境应依赖 migrate deploy + 已提交的 migrations 文件夹 ,而不是 db push

6.5 prisma/migrations 目录里有什么

bash 复制代码
prisma/migrations/
├── migration_lock.toml       # 锁定数据库提供方(如 mysql)
└── 20240323120000_init/
    └── migration.sql         # 本次迁移的 SQL(由 Prisma 生成)
  • migration.sql:可阅读、可审计;一般不要手改(除非你很清楚后果)。
  • 新增迁移 = 新文件夹 + 新 SQL,按时间顺序应用。

6.6 常见注意点

  • 修改已部署 过的迁移文件可能导致校验失败;已上线的变更应通过新迁移追加修改。
  • 备份:生产执行 migrate deploy 前应有数据库备份策略。
  • Serverless(如部分 Vercel 函数)需注意 MySQL 连接数;必要时使用连接池或官方推荐的托管方案。

七、Prisma Client 单例(避免开发环境连接耗尽)

lib/prisma.ts

typescript 复制代码
import { PrismaClient } from "@prisma/client";

const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };

export const prisma =
  globalForPrisma.prisma ??
  new PrismaClient({
    log: process.env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
  });

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;

八、在 Server Component 中查询

app/users/page.tsx

typescript 复制代码
import { prisma } from "@/lib/prisma";

export default async function UsersPage() {
  const users = await prisma.user.findMany({
    orderBy: { createdAt: "desc" },
    include: { posts: true },
  });

  return (
    <main>
      <h1>用户列表</h1>
      <ul>
        {users.map((u) => (
          <li key={u.id}>
            {u.name ?? u.email} --- 文章数:{u.posts.length}
          </li>
        ))}
      </ul>
    </main>
  );
}

九、Route Handler 示例

app/api/users/route.ts

typescript 复制代码
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";

export async function GET() {
  const users = await prisma.user.findMany();
  return NextResponse.json(users);
}

export async function POST(request: Request) {
  const body = await request.json();
  const { email, name } = body as { email: string; name?: string };

  const user = await prisma.user.create({
    data: { email, name },
  });

  return NextResponse.json(user, { status: 201 });
}

十、数据的修改与删除接口(Route Handlers)

单条资源的 更新删除 通常不放在集合路径 /api/users 上,而是使用 动态路由 /api/users/[id]:路径里的 id 对应数据库主键(或业务唯一标识),与 REST 习惯一致(对「某一个用户」做 PATCH/DELETE)。

10.1 路由文件位置

在 App Router 中新建:

bash 复制代码
app/api/users/[id]/route.ts

同一文件内可导出 PATCH (部分更新)、PUT (全量更新,可选)、DELETE(删除)。Next.js 按 HTTP 方法分发到对应导出函数。

10.2 PATCH:部分更新(常用)

客户端只传需要改的字段(例如只改 name),服务端用 prisma.user.update 合并进数据库。

要点

  • where :用 id 定位一行;若不存在,Prisma 抛出 P2025 (Record not found),应转为 404
  • data :只写入请求体里出现的字段,避免把未传字段覆盖成 null(需自行从 body 解构并组装 data)。

app/api/users/[id]/route.ts 示例(Next.js 15+ 中 paramsPromise ,需 await):

typescript 复制代码
import { NextResponse } from "next/server";
import { Prisma } from "@prisma/client";
import { prisma } from "@/lib/prisma";

type Ctx = { params: Promise<{ id: string }> };

export async function PATCH(request: Request, ctx: Ctx) {
  const { id } = await ctx.params;
  const userId = Number(id);
  if (Number.isNaN(userId)) {
    return NextResponse.json({ error: "Invalid id" }, { status: 400 });
  }

  const body = await request.json();
  const { name, email } = body as { name?: string; email?: string };

  const data: { name?: string; email?: string } = {};
  if (name !== undefined) data.name = name;
  if (email !== undefined) data.email = email;
  if (Object.keys(data).length === 0) {
    return NextResponse.json({ error: "No fields to update" }, { status: 400 });
  }

  try {
    const user = await prisma.user.update({
      where: { id: userId },
      data,
    });
    return NextResponse.json(user);
  } catch (e) {
    if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") {
      return NextResponse.json({ error: "User not found" }, { status: 404 });
    }
    throw e;
  }
}

调用示例(前端或其他客户端):

http 复制代码
PATCH /api/users/1 HTTP/1.1
Content-Type: application/json

{"name": "新名字"}

10.3 PUT:全量更新(可选)

语义上 PUT 常表示「用请求体替换 整个资源」。若 User 只有 emailname 等少量字段,可以在服务端规定:必须 带上全部可写字段,否则 400 ;再执行 update

若业务上更关心「只改部分字段」,优先用 PATCH,避免客户端漏传字段导致误清空。

10.4 DELETE:删除一条记录

使用 prisma.user.delete ,成功时返回 204 No Content (无响应体)较常见;也可返回 200 并带上已删除对象的 JSON,按团队约定即可。

typescript 复制代码
import { NextResponse } from "next/server";
import { Prisma } from "@prisma/client";
import { prisma } from "@/lib/prisma";

type Ctx = { params: Promise<{ id: string }> };

export async function DELETE(_request: Request, ctx: Ctx) {
  const { id } = await ctx.params;
  const userId = Number(id);
  if (Number.isNaN(userId)) {
    return NextResponse.json({ error: "Invalid id" }, { status: 400 });
  }

  try {
    await prisma.user.delete({
      where: { id: userId },
    });
    return new NextResponse(null, { status: 204 });
  } catch (e) {
    if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") {
      return NextResponse.json({ error: "User not found" }, { status: 404 });
    }
    throw e;
  }
}

说明 :若表上有 外键约束 (例如 Post 依赖 User),删除用户可能触发 Prisma/MySQL 外键错误(如 P2003 ),需在业务层先删子表数据、或改用数据库 ON DELETE CASCADE 、或禁止删除,并对该错误返回 409 等状态码。

10.5 按条件批量更新 / 删除(updateMany / deleteMany)

不通过 id 单条操作,而是按条件批量处理时使用:

typescript 复制代码
// 将所有未发布文章标为已发布(示例)
await prisma.post.updateMany({
  where: { published: false },
  data: { published: true },
});

// 删除某邮箱前缀的测试用户(慎用,生产需强校验)
await prisma.user.deleteMany({
  where: { email: { startsWith: "test_" } },
});

要点updateMany / deleteMany 不会 在 0 行匹配时抛错;若接口需要「至少删一行」再返回 404,需在执行后根据 count 自行判断。

10.6 Next.js 14 与 params 类型说明

若项目仍为 Next.js 14 ,部分版本中 params同步对象 ,签名可写为 { params: { id: string } }不使用 await params。升级到 Next.js 15 后请改为 params: Promise<...>await params,与官方 Route Handlers 类型一致。

10.7 前端请求代码(修改与删除)

以下使用浏览器原生 fetch ,适用于 客户端组件 (文件顶部需加 "use client" )。请求路径与上文 Route Handler 一致:/api/users/[id]

要点

  • PATCH :设置 Content-Type: application/jsonbodyJSON.stringify 后的对象。
  • DELETE :若服务端返回 204 No Content没有响应体 ,不要调用 response.json() ,根据 response.okresponse.status === 204 判断成功。
  • 失败时(4xx/5xx)若接口返回 JSON 错误信息,可 await response.json() 读取(注意先判断 Content-Typetry/catch)。

封装函数(可放在 lib/user-api.ts 或组件同文件内)

typescript 复制代码
/** 部分更新用户(对应 PATCH /api/users/:id) */
export async function updateUser(
  id: number,
  data: { name?: string; email?: string }
): Promise<{ id: number; name: string | null; email: string }> {
  const res = await fetch(`/api/users/${id}`, {
    method: "PATCH",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(data),
  });

  const payload = await res.json().catch(() => ({}));

  if (!res.ok) {
    const message =
      typeof payload === "object" && payload !== null && "error" in payload
        ? String((payload as { error: unknown }).error)
        : res.statusText;
    throw new Error(message || `HTTP ${res.status}`);
  }

  return payload as { id: number; name: string | null; email: string };
}

/** 删除用户(对应 DELETE /api/users/:id,成功为 204 无 body) */
export async function deleteUser(id: number): Promise<void> {
  const res = await fetch(`/api/users/${id}`, {
    method: "DELETE",
  });

  if (res.ok) {
    return;
  }

  let message = res.statusText;
  try {
    const payload = (await res.json()) as { error?: string };
    if (payload.error) message = payload.error;
  } catch {
    /* 无 JSON 时忽略 */
  }
  throw new Error(message || `HTTP ${res.status}`);
}

在客户端组件中调用(示例)

tsx 复制代码
"use client";

import { useState } from "react";
import { deleteUser, updateUser } from "@/lib/user-api";

export function UserActions({ userId }: { userId: number }) {
  const [loading, setLoading] = useState<"patch" | "delete" | null>(null);
  const [error, setError] = useState<string | null>(null);

  async function handleRename() {
    setError(null);
    setLoading("patch");
    try {
      const user = await updateUser(userId, { name: "新名字" });
      console.log("更新成功", user);
      // 例如:router.refresh() 或更新本地列表状态
    } catch (e) {
      setError(e instanceof Error ? e.message : "更新失败");
    } finally {
      setLoading(null);
    }
  }

  async function handleDelete() {
    if (!confirm("确定删除该用户?")) return;
    setError(null);
    setLoading("delete");
    try {
      await deleteUser(userId);
      console.log("删除成功");
      // 例如:跳转列表页或从状态中移除
    } catch (e) {
      setError(e instanceof Error ? e.message : "删除失败");
    } finally {
      setLoading(null);
    }
  }

  return (
    <div>
      {error && <p role="alert">{error}</p>}
      <button type="button" onClick={handleRename} disabled={loading !== null}>
        {loading === "patch" ? "更新中..." : "修改名字"}
      </button>
      <button type="button" onClick={handleDelete} disabled={loading !== null}>
        {loading === "delete" ? "删除中..." : "删除"}
      </button>
    </div>
  );
}

说明 :若列表数据来自 Server Component ,删除/更新成功后可在子组件中调用 import { useRouter } from "next/navigation"router.refresh(),让服务端重新渲染最新数据;或使用全局状态 / React Query 等自行同步列表。


十一、事务示例

typescript 复制代码
import { prisma } from "@/lib/prisma";

export async function createUserWithPost(email: string, postTitle: string) {
  return prisma.$transaction(async (tx) => {
    const user = await tx.user.create({
      data: { email },
    });
    await tx.post.create({
      data: {
        title: postTitle,
        authorId: user.id,
      },
    });
    return user;
  });
}

十二、小结

  • 从官网安装 MySQL ServerMySQL Workbench ,用 Workbench 测试连接建库执行 SQL 与查看表数据;下载页MySQL Community ServerMySQL Installer(Windows)MySQL Workbench
  • create-next-app 初始化项目,App Router 下页面在 app/ ,API 在 app/api/.../route.ts
  • Prismaschema.prisma 定义模型;migrate dev 在开发中生成并应用迁移;migrate deploy 在生产/CI 应用已提交的迁移。
  • MySQL 通过 DATABASE_URL 连接;全栈链路为:Next(服务端)→ Prisma Client → MySQL。
  • 单条资源 的修改与删除使用 app/api/.../[id]/route.ts ,导出 PATCH / DELETE (可选 PUT );不存在时处理 Prisma P2025 返回 404 ;批量操作用 updateMany / deleteMany 。前端在客户端组件中用 fetchPATCH (JSON body)与 DELETE204 无响应体时不要 response.json()
  • prisma 官网:www.prisma.io/docs
  • next 官网 nextjs.frontendx.cn/

相关推荐
FPGA小迷弟1 小时前
FPGA 时序约束基础:从时钟定义到输入输出延迟的完整设置
前端·学习·fpga开发·verilog·fpga
毛骗导演2 小时前
@tencent-weixin/openclaw-weixin 插件深度解析(四):API 协议与数据流设计
前端·架构
毛骗导演2 小时前
@tencent-weixin/openclaw-weixin 插件深度解析(二):消息处理系统架构
前端·架构
IT_陈寒2 小时前
深入理解JavaScript:核心原理与最佳实践
前端·人工智能·后端
MrGud2 小时前
Cesium中的坐标系及其转换
前端·cesium
小付学代码2 小时前
香港地图可编辑版
前端
兆子龙2 小时前
TypeScript高级类型编程:从入门到精通
前端·后端
SuperEugene2 小时前
Vue3 模板语法规范实战:v-if/v-for 不混用 + 表达式精简,避坑指南|Vue 组件与模板规范篇
开发语言·前端·javascript·vue.js·前端框架
IT_陈寒3 小时前
Python开发者的效率革命:这5个技巧让你的代码提速50%!
前端·人工智能·后端