你真以为“全栈”就是前端多写两个接口就完事了?
🏆本文收录于《滚雪球学SpringBoot 3》:
https://blog.csdn.net/weixin_43970743/category_12795608.html,专门攻坚指数提升,本年度国内最系统+最专业+最详细(永久更新)。
本专栏致力打造最硬核 SpringBoot3 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。 如果想快速定位学习,可以看这篇【SpringBoot3教程导航帖】https://blog.csdn.net/weixin_43970743/article/details/151115907,你想学习的都被收集在内,快速投入学习!!两不误。
若还想学习更多,可直接前往《滚雪球学SpringBoot(全版本合集)》:https://blog.csdn.net/weixin_43970743/category_11599389.html,涵盖SpringBoot所有版本教学文章。
演示环境说明:
- 开发工具:IDEA 2021.3
- JDK版本: JDK 17(推荐使用 JDK 17 或更高版本,因为 Spring Boot 3.x 系列要求 Java 17,Spring Boot 3.5.4 基于 Spring Framework 6.x 和 Jakarta EE 9,它们都要求至少 JDK 17。)
- Spring Boot版本:3.5.4(于25年7月24日发布)
- Maven版本:3.8.2 (或更高)
- Gradle:(如果使用 Gradle 构建工具的话):推荐使用 Gradle 7.5 或更高版本,确保与 JDK 17 兼容。
- 操作系统:Windows 11
前言:我为什么还在写“全栈入门”?
说出来你可能不信:我并不爱写入门文。入门文太容易写得轻飘飘——贴几段代码、跑起来、截图、完结撒花。读者是爽了,但一遇到真实业务的泥潭:分页、鉴权、并发、迁移、缓存、环境变量、容器化、线上日志、冷启动……立刻就开始“老师我报错了”。
我更想写的是那种:
你读的时候会忍不住点头:“对对对,我之前就是栽在这儿。”
你写的时候会稍微皱眉:“原来这里应该这样设计才不挨打。”
你上线的时候能松口气:“至少这玩意儿不是纸糊的。”
我们用的技术栈如下:
- Next.js(App Router + Route Handlers):路由、服务端渲染、接口一体化。官方文档明确 Route Handlers 基于 Web Request/Response API,且位于
app目录中。 - Prisma ORM + Postgres:类型安全的数据访问、迁移与建模;Prisma 官方有 Next.js + Prisma Postgres 的完整指南。
- PostgreSQL 认证与访问控制:别把“连上库”当胜利,认证与访问策略是底线。
- Docker(Dockerfile & 镜像分层思维):把“我电脑能跑”升级成“到哪儿都能跑”。
接下来,我们做一个“认真但不沉闷”的小项目:任务清单(Task Board)。它不伟大,但足够覆盖全栈最常见的坑。
1. 需求别急着写代码:先把“边界”讲明白
任务清单听起来就四个字:增删改查。
但你要是把它当成纯 CRUD,那我只能说:你很勇。🙂
我们先定一个最小可行但“像样”的范围:
- 任务模型:标题、描述、状态(TODO/DOING/DONE)、创建时间、更新时间
- 列表接口:支持分页、按状态筛选、按时间排序
- 写接口:创建、更新状态、删除(软删可选)
- 防呆:字段校验(长度、空值)、统一错误返回
- 安全边界:先不做复杂登录,但至少要避免“把数据库密码写进前端包里”这种社死行为
- 部署形态:Docker 一键起(含数据库),别让读者装环境装到怀疑人生
你看,光是这么一列,工程味儿就出来了——这才是“全栈”的起点:不是功能多,而是思路完整。
2. 项目骨架:Next.js App Router 为什么舒服(也为什么容易踩坑)
Next.js 的 App Router 把页面与服务端能力融合得很自然:Server Components、Layouts、Route Handlers……让你写全栈像在写一棵树。官方也明确它基于 React Server Components,并围绕 app 目录工作。
2.1 初始化项目
npx create-next-app@latest task-board
cd task-board
目录建议(关键点):
task-board/
app/
page.tsx
api/
tasks/
route.ts
lib/
db.ts
prisma/
schema.prisma
app/api/**/route.ts:Route Handlers 的标准位置与写法(支持 GET/POST/PUT/PATCH/DELETE 等)。lib/db.ts:数据库 client 的统一出口,避免到处 new client(不然热更新时你会看到连接像野草一样长)
3. 数据建模:Prisma + Postgres,别让“表结构”靠灵感
我见过最离谱的事之一:有人说“字段先随便写,后面再改”。
我当时差点把咖啡喷出来:你以为数据库迁移是改 Word 文档吗?
Prisma 的好处是:模型清晰、迁移可追踪,且官方对 Next.js 的整合路径写得很细。
3.1 安装 Prisma
npm i prisma @prisma/client
npx prisma init
3.2 Prisma Schema(prisma/schema.prisma)
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
enum TaskStatus {
TODO
DOING
DONE
}
model Task {
id String @id @default(cuid())
title String
detail String?
status TaskStatus @default(TODO)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
这段建模其实在“克制”上很重要:
id用cuid():前端友好、分布式友好updatedAt @updatedAt:别手写更新时间,写一次错一次enum:把状态限定住,避免未来出现Doing、DOIN、do_ing这种“人类迷惑行为”
3.3 迁移与生成 Client
npx prisma migrate dev --name init
npx prisma generate
4. 数据库连接:别把连接池当摆设(也别把密码当装饰)
PostgreSQL 的认证与访问控制不是“高级话题”,它是底裤。
官方文档把认证与 pg_hba.conf 讲得很清楚:你允许谁从哪儿连、以什么身份连、用什么认证方式连,都能配置。
我们在开发环境用 Docker 起 Postgres,并把连接串放进 .env:
.env
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/taskboard?schema=public"
注意:
- 连接串只在服务端用。Next.js 里 Route Handlers 在服务端执行,别把它 import 到 Client Component 里。
5. 统一 Prisma Client:不然热更新会把你逼疯
lib/db.ts
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: ["error", "warn"],
});
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
这段的意义就一句话:
开发环境热更新别重复创建连接。
你要是见过日志里连接数飙升,你会明白这不是“优化”,这是“救命”。
6. Route Handlers:接口不是“写出来就行”,要“写得像能维护”
Next.js 官方说 Route Handlers 允许你为特定路由写自定义请求处理,并使用 Web Request/Response API。
我们写两个端点:
GET /api/tasks:分页 + 筛选POST /api/tasks:创建任务
app/api/tasks/route.ts
import { prisma } from "@/lib/db";
function badRequest(message: string) {
return Response.json({ ok: false, message }, { status: 400 });
}
export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const status = searchParams.get("status"); // TODO/DOING/DONE
const page = Number(searchParams.get("page") ?? "1");
const pageSize = Math.min(Number(searchParams.get("pageSize") ?? "10"), 50);
if (!Number.isFinite(page) || page < 1) return badRequest("page 参数不合法");
if (!Number.isFinite(pageSize) || pageSize < 1) return badRequest("pageSize 参数不合法");
const where =
status && ["TODO", "DOING", "DONE"].includes(status)
? { status: status as any }
: {};
const [total, items] = await Promise.all([
prisma.task.count({ where }),
prisma.task.findMany({
where,
orderBy: { createdAt: "desc" },
skip: (page - 1) * pageSize,
take: pageSize,
}),
]);
return Response.json({
ok: true,
data: { total, page, pageSize, items },
});
}
export async function POST(req: Request) {
const body = await req.json().catch(() => null);
if (!body) return badRequest("请求体不是合法 JSON");
const title = String(body.title ?? "").trim();
const detail = body.detail ? String(body.detail).trim() : null;
if (!title) return badRequest("title 不能为空");
if (title.length > 80) return badRequest("title 太长了(<=80)");
if (detail && detail.length > 500) return badRequest("detail 太长了(<=500)");
const task = await prisma.task.create({
data: { title, detail: detail ?? undefined },
});
return Response.json({ ok: true, data: task }, { status: 201 });
}
你会发现我写了不少“啰嗦”的校验。
是的,我就是故意的。因为线上最常见的事故之一就是:
“前端传了个奇怪的东西,后端没校验,数据库里塞进了更奇怪的东西,最后报错还找不到是谁干的。”
7. 前端页面:别把数据请求写得像在掷骰子
app/page.tsx(为了简单,这里用客户端请求;如果你想更进一步,可以改为 Server Component 直取数据)
"use client";
import { useEffect, useMemo, useState } from "react";
type Task = {
id: string;
title: string;
detail: string | null;
status: "TODO" | "DOING" | "DONE";
createdAt: string;
};
export default function Page() {
const [status, setStatus] = useState<"" | Task["status"]>("");
const [title, setTitle] = useState("");
const [items, setItems] = useState<Task[]>([]);
const [loading, setLoading] = useState(false);
const qs = useMemo(() => {
const p = new URLSearchParams();
if (status) p.set("status", status);
p.set("page", "1");
p.set("pageSize", "10");
return p.toString();
}, [status]);
async function load() {
setLoading(true);
try {
const res = await fetch(`/api/tasks?${qs}`);
const json = await res.json();
setItems(json?.data?.items ?? []);
} finally {
setLoading(false);
}
}
async function create() {
const t = title.trim();
if (!t) return alert("标题别空着呀(我会尴尬)");
const res = await fetch("/api/tasks", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: t }),
});
if (!res.ok) {
const json = await res.json().catch(() => ({}));
return alert(json?.message ?? "创建失败");
}
setTitle("");
await load();
}
useEffect(() => {
load();
}, [qs]);
return (
<main style={{ maxWidth: 720, margin: "40px auto", padding: 16 }}>
<h1 style={{ fontSize: 28, marginBottom: 12 }}>Task Board</h1>
<div style={{ display: "flex", gap: 8, marginBottom: 12 }}>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="写点什么任务…(别写“摸鱼”,我怕你老板看到)"
style={{ flex: 1, padding: 10 }}
/>
<button onClick={create} style={{ padding: "10px 14px" }}>
添加
</button>
</div>
<div style={{ display: "flex", gap: 8, marginBottom: 12 }}>
<button onClick={() => setStatus("")}>全部</button>
<button onClick={() => setStatus("TODO")}>TODO</button>
<button onClick={() => setStatus("DOING")}>DOING</button>
<button onClick={() => setStatus("DONE")}>DONE</button>
</div>
{loading ? <p>加载中…我在努力了😮💨</p> : null}
<ul style={{ paddingLeft: 18 }}>
{items.map((t) => (
<li key={t.id} style={{ marginBottom: 10 }}>
<b>[{t.status}]</b> {t.title}
<div style={{ fontSize: 12, opacity: 0.7 }}>
{new Date(t.createdAt).toLocaleString()}
</div>
</li>
))}
</ul>
</main>
);
}
这页面不花哨,但有三个好处:
- 接口写得对不对,一眼就能验证
- 前后端联通后,再做复杂 UI 才有意义
- 你不会在 CSS 上浪费一下午(真的,CSS 会吃人)
8. 容器化:把“能跑”变成“到哪儿都能跑”
Docker 官方文档把 Dockerfile 的指令、镜像分层、构建概念讲得很系统。
8.1 Dockerfile(生产构建思路:依赖层缓存 + 构建层 + 运行层)
Dockerfile
# 1) deps
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# 2) build
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# 3) runner
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["npm", "run", "start"]
这三段式写法的“人话解释”就是:
- 依赖装一次,能缓存就缓存(不然每次构建都慢得想砸键盘)
- 构建阶段做编译
- 运行阶段尽量干净(不把无关文件塞进去)
8.2 docker-compose:把数据库也带上
docker-compose.yml
services:
db:
image: postgres:16
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: taskboard
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
web:
build: .
environment:
DATABASE_URL: postgresql://postgres:postgres@db:5432/taskboard?schema=public
ports:
- "3000:3000"
depends_on:
- db
volumes:
pgdata:
启动:
docker compose up --build
9. 别急着庆祝:全栈“能上线”才算数(至少把这些坑提前填了)
到这里,项目基本跑起来了。你可能会有点兴奋,我也理解。
但我还是得泼一小瓢冷水:跑起来 ≠ 可靠。
下面这些点,你哪怕只做一半,项目的“抗揍性”都会明显提升:
- 统一错误结构:现在我们
badRequest还算统一,但进一步可以扩展错误码、追踪 ID - 输入校验库:例如 zod(这里我没引入,是为了让读者先看清“该校验什么”)
- 数据库索引:当你开始按
status、createdAt查询时,索引就是性能的空气 - 并发与事务:多个操作组合时,用事务保证一致性
- 鉴权:哪怕是最简 token,也别让“谁都能删库”成为你的项目特色
- 配置与密钥管理:不要把
.env提交到仓库;更不要在客户端代码里读DATABASE_URL
PostgreSQL 在认证和访问控制上给了非常多选择,包括多种认证方法与 pg_hba.conf 的配置方式。你越早理解它,越不容易把安全当成“上线后再说”。
10. 结语:你要的“全栈”,到底想解决什么问题?
写到这儿我有点感慨。
“全栈”从来不是一个炫技标签,它更像一种态度:
我不只关心界面好不好看,也关心数据怎么存、接口怎么扛、部署怎么稳、出事怎么查。
🧧福利赠与你🧧
无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学SpringBoot」,bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门SpringBoot,就像滚雪球一样,越滚越大, 无边无际,指数级提升。
最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。
同时欢迎大家关注公众号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G PDF编程电子书、简历模板、技术文章Markdown文档等海量资料。
ps:本文涉及所有源代码,均已上传至Gitee:
https://gitee.com/bugjun01/SpringBoot-demo开源,供同学们一对一参考 Gitee传送门https://gitee.com/bugjun01/SpringBoot-demo,同时,原创开源不易,欢迎给个star🌟,想体验下被🌟的感jio,非常感谢❗
🫵 Who am I?
我是 bug菌:
- 热活跃于 CSDN:
https://blog.csdn.net/weixin_43970743| 掘金:https://juejin.cn/user/695333581765240| InfoQ:https://www.infoq.cn/profile/4F581734D60B28/publish| 51CTO:https://blog.51cto.com/u_15700751| 华为云:https://bbs.huaweicloud.com/community/usersnew/id_1582617489455371| 阿里云:https://developer.aliyun.com/profile/uolxikq5k3gke| 腾讯云:https://cloud.tencent.com/developer/user/10216480/articles等技术社区; - CSDN 博客之星 Top30、华为云多年度十佳博主&卓越贡献奖、掘金多年度人气作者 Top40;
- 掘金、InfoQ、51CTO 等平台签约及优质作者;
- 全网粉丝累计 30w+。
更多高质量技术内容及成长资料,可查看这个合集入口 👉 点击查看:https://bbs.csdn.net/topics/612438251 👈️
硬核技术公众号 「猿圈奇妙屋」https://bbs.csdn.net/topics/612438251 期待你的加入,一起进阶、一起打怪升级。
- End -
- 点赞
- 收藏
- 关注作者
评论(0)