本文介绍如何用 Next.js 、Prisma 、MySQL 搭建全栈应用:包含 MySQL 与 MySQL Workbench 的安装与基本使用 、创建项目、目录含义、数据库建模、Migrations(迁移) 的工作方式与命令,并附代码示例。
一、技术栈分工
| 技术 | 作用 |
|---|---|
| Next.js | React 框架:路由、SSR、服务端组件、Route Handlers(API) |
| Prisma | ORM:类型安全查询、Schema 定义、迁移版本管理 |
| MySQL | 关系型数据库:表、外键、事务、索引 |
二、如何创建 Next.js 项目
2.1 前置条件
- 已安装 Node.js (建议 LTS 版本)
- 包管理器任选:
npm、pnpm或yarn
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/,则把 app、components 等放在 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 为例(适合本机开发):
- 打开 MySQL Installer 下载页,选择 Windows (x86, 64-bit), MSI Installer(体积较大的完整安装包或在线安装包均可)。
- 运行安装程序,选择安装类型:
- Developer Default:会安装 Server、Workbench、Shell 等,适合开发。
- 若只需数据库服务,可选 Server only。
- 按向导执行 Execute 安装所选组件。
- 在 Type and Networking 中保持默认端口 3306 (除非端口冲突,需与 Prisma 的
DATABASE_URL一致)。 - 在 Authentication 中保持默认 Strong Password(推荐)。
- 设置 root 用户密码并牢记;后续 Workbench、Prisma 连接都会用到。
- 将 MySQL 配置为 Windows Service ,并勾选 Start the MySQL Server at System Startup(开机自启,便于开发)。
- 完成安装。
4.3 安装后如何确认 MySQL 已运行
- 服务 :按
Win + R,输入services.msc,查找 MySQL 或 MySQL80 等服务名,状态应为 正在运行。 - 命令行(若安装时勾选了命令行客户端且已加入 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 连接数据库
- 打开 MySQL Workbench。
- 主界面 MySQL Connections 区域点击 「+」 或 MySQL Connections 旁的加号,新建连接。
- 填写连接参数(本地开发常见值):
| 字段 | 典型值 | 说明 |
|---|---|---|
| Connection Name | 任意名称,如 Local Dev |
仅显示用 |
| Hostname | 127.0.0.1 或 localhost |
本机数据库 |
| Port | 3306 |
与安装时一致 |
| Username | root |
或你创建的其他用户 |
| Password | --- | 点击 Store in Keychain / Vault... 保存密码,避免每次输入 |
- 点击 Test Connection ,提示成功即可 OK 保存。
- 双击该连接进入主工作区:左侧为 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 Inspector 或 Alter 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
该命令会:
- 对比当前 schema 与数据库状态,生成 新的 SQL 迁移文件到
prisma/migrations/<时间戳>_<name>/migration.sql。 - 应用 这些迁移到当前
DATABASE_URL指向的数据库。 - 触发
prisma generate,更新 Prisma Client。
适合:本地开发 、共享开发数据库 (团队约定好同一条 DATABASE_URL 时要注意冲突)。
6.3 生产 / CI:prisma migrate deploy
在生产构建流程 或服务器上,不要 用 migrate dev(它会交互式、且偏向开发工作流)。应使用:
bash
npx prisma migrate deploy
作用 :只执行 prisma/migrations 里尚未应用 的迁移,不根据当前数据库反向改 schema,适合流水线与只读权限受限的环境。
典型顺序:
- 构建应用:
npm run build - 部署前或启动前:
npx prisma migrate deploy - 启动:
npm run start
6.4 db push 与 migrate 的区别
| 命令 | 适用场景 |
|---|---|
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+ 中 params 为 Promise ,需 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 只有 email、name 等少量字段,可以在服务端规定:必须 带上全部可写字段,否则 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/json,body为JSON.stringify后的对象。DELETE:若服务端返回 204 No Content ,没有响应体 ,不要调用response.json(),根据response.ok或response.status === 204判断成功。- 失败时(4xx/5xx)若接口返回 JSON 错误信息,可
await response.json()读取(注意先判断Content-Type或try/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 Server 与 MySQL Workbench ,用 Workbench 测试连接 、建库 、执行 SQL 与查看表数据;下载页 :MySQL Community Server、MySQL Installer(Windows)、MySQL Workbench。
- 用
create-next-app初始化项目,App Router 下页面在app/,API 在app/api/.../route.ts。 - Prisma 用
schema.prisma定义模型;migrate dev在开发中生成并应用迁移;migrate deploy在生产/CI 应用已提交的迁移。 - MySQL 通过
DATABASE_URL连接;全栈链路为:Next(服务端)→ Prisma Client → MySQL。 - 单条资源 的修改与删除使用
app/api/.../[id]/route.ts,导出PATCH/DELETE(可选PUT);不存在时处理 PrismaP2025返回 404 ;批量操作用updateMany/deleteMany。前端在客户端组件中用fetch发PATCH(JSON body)与DELETE,204 无响应体时不要response.json()。 - prisma 官网:www.prisma.io/docs
- next 官网 nextjs.frontendx.cn/