你真的以为“全栈”只是把前后端粘一起就完事了吗?
🏆本文收录于《滚雪球学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
前言:全栈这碗饭,真没你想的那么“香”,但也没那么“玄”
我见过太多人一开口就说自己全栈,语气比咖啡还浓:“我前端 React、后端 Node、数据库也懂点”。
我通常不会当场拆台,毕竟成年人嘛,留点体面——但我心里会默默补一句:**“懂点”到底是懂到能扛上线,还是懂到能扛住线上炸?”**🙃
说白了,“全栈”不是“技能点摊得很薄”,而是你能把一个产品从 0 → 可用 → 可上线 → 可维护 → 可扩展 这条链路打通。你得考虑:
- 前端渲染策略怎么选(SSR / RSC / CSR / 静态),怎么降低 JS 负担
- 后端怎么分层、怎么验证、怎么限流、怎么记录日志、怎么做错误治理
- 数据库怎么建模、怎么迁移、怎么索引、怎么避免慢查询
- 环境变量怎么管、容器怎么跑、CI 怎么测、怎么避免“在我电脑上可以”
今天我就用一个“能落地”的项目骨架,把这些串起来。你会看到一堆代码,当然也会看到我一路吐槽——因为工程这玩意儿,不吐槽两句真干不下去😂。
1. 技术选型:我为啥选 Next.js + NestJS + Prisma + Postgres?
先别急着喷“又是 Next.js”,我知道你想说什么:“现在谁还不用它啊?”
但选型不是追潮流,是为了减少你未来的返工概率。
1.1 前端:Next.js App Router ——别只会“pages”,不然你像拿着诺基亚拍 8K
- App Router 天生拥抱 React 的 Server Components、流式渲染、缓存策略等能力。Next.js 文档明确说明:App Router 是基于文件系统的路由,利用了 React 的新特性(Server Components、Suspense、Server Functions 等)。
- 默认页面与布局是 Server Components:你可以把“数据获取 + 渲染”放在服务端,减少客户端 bundle。
我自己的口头禅:
“能在服务端做的事,就别让浏览器背锅。”
浏览器的命也是命。
1.2 后端:NestJS ——结构化到你想哭,但哭完你会感谢它
NestJS 的核心价值不是“它很 Node”,而是它把后端工程化的那套(模块、依赖注入、分层)直接摆你脸上。官方文档就是这么设计的:模块化组织、Provider(服务)作为核心概念。
如果你写过“一个文件 2000 行,谁改谁挨骂”的 Node 项目,Nest 会让你像从城中村搬进电梯房——
不一定更快乐,但更体面。
1.3 ORM:Prisma ——迁移、类型、安全感,一套带走
Prisma Migrate 是 Prisma 官方主推的迁移工具,和 schema 建模强绑定,开发环境上手很顺。 ([Prisma][4])
你当然也可以手写 SQL 迁移,当然也可以“我就喜欢 SQL 的粗粝感”。
但团队协作时,Prisma 的“规范感”能减少很多口水仗。
1.4 数据库:PostgreSQL ——稳重、强大、还能把 JSONB 玩出花
聊性能绕不开索引,PostgreSQL 官方文档对 GIN 索引解释得很清楚:它适合处理复合值,并在复合项内部搜索元素值。
你后面会看到我用 JSONB + GIN 做“可扩展字段”的实践。
因为现实世界里,“需求永远会变”,而你不可能每次都重构表结构。
1.5 部署与协作:Docker Compose + GitHub Actions ——少点玄学,多点自动化
- Docker Compose 文件格式与多容器编排细节官方参考写得很细。
- GitHub Actions 文档明确支持 CI/CD 工作流编排。
2. 项目目标:做一个“轻量但真能用”的创作后台 + 内容展示
我们来做个小但完整的产品:Creator Hub(创作中台 + 展示)。
功能清单(别怕,后面都有代码)
- 用户登录(先做邮箱+密码示例,生产建议 MFA/SSO 视场景)
- 创建内容(草稿/发布)
- 内容列表与详情(支持搜索)
- 审计日志(谁改了什么)
- 运行方式:本地一条命令启动(docker compose up)
- CI:自动 lint / test / build
我知道你想加“点赞收藏评论关注私信”,先按住——
**全栈最怕的不是功能多,是链路不通。**链路通了你加什么都不慌。
3. 目录结构:别整花活,清晰比“酷”重要
creator-hub/
apps/
web/ # Next.js App Router
api/ # NestJS
packages/
shared/ # DTO、工具函数、zod schema 等共享
infra/
docker/ # compose, nginx(可选)
.github/
workflows/
README.md
这里我特意用了 monorepo(你也可以不用)。
我这么做的原因很直白:类型共享和统一工程规范更轻松,尤其是 DTO、验证规则、错误码这些“容易扯皮”的东西。
4. 前端(Next.js App Router):RSC 不是玄学,是“把锅还给服务器”
4.1 页面默认是 Server Component:减少客户端 JS
Next.js 文档说明:App Router 默认布局和页面是 Server Components,适合服务端取数并渲染,再把结果流式传给客户端。
我们做一个内容列表页:
apps/web/app/(site)/posts/page.tsx
import Link from "next/link";
type Post = {
id: string;
title: string;
excerpt: string;
createdAt: string;
};
async function fetchPosts(): Promise<Post[]> {
// 这里走后端 API,演示用
const res = await fetch(`${process.env.API_BASE_URL}/posts`, {
// App Router 的 fetch 默认可被缓存/去重(具体策略看你的设置)
cache: "no-store",
});
if (!res.ok) throw new Error("Failed to fetch posts");
return res.json();
}
export default async function PostsPage() {
const posts = await fetchPosts();
return (
<main style={{ padding: 24 }}>
<h1>Posts</h1>
<p style={{ opacity: 0.7 }}>
这页是 Server Component:浏览器少背点 JS 包袱,我心里也踏实点。
</p>
<ul style={{ marginTop: 16 }}>
{posts.map((p) => (
<li key={p.id} style={{ marginBottom: 12 }}>
<Link href={`/posts/${p.id}`}>
<strong>{p.title}</strong>
</Link>
<div style={{ opacity: 0.75 }}>{p.excerpt}</div>
<small style={{ opacity: 0.6 }}>{new Date(p.createdAt).toLocaleString()}</small>
</li>
))}
</ul>
</main>
);
}
这段代码的“爽点”在于:
- 页面渲染在服务端完成
- 数据获取在服务端完成
- 客户端几乎不用 hydration(除非你放交互组件)
你可能会问:“那我想要搜索框这种交互怎么办?”
别急,Client Component 上场。
4.2 只把“需要交互”的部分做成 Client Component
Next.js 官方建议:"use client" 是边界声明,别把整页都拖进客户端 bundle。
apps/web/app/(site)/posts/SearchBox.tsx
"use client";
import { useMemo, useState } from "react";
export default function SearchBox({
onChange,
}: {
onChange: (keyword: string) => void;
}) {
const [value, setValue] = useState("");
const placeholder = useMemo(() => {
const arr = ["搜标题?", "搜关键词?", "搜你忘了写啥的草稿?"];
return arr[Math.floor(Math.random() * arr.length)];
}, []);
return (
<div style={{ margin: "16px 0" }}>
<input
value={value}
placeholder={placeholder}
onChange={(e) => {
const v = e.target.value;
setValue(v);
onChange(v);
}}
style={{
padding: 10,
width: "100%",
maxWidth: 520,
borderRadius: 10,
border: "1px solid #ddd",
}}
/>
</div>
);
}
然后在 Server Component 页面里引入它(注意:Server Component 可以渲染 Client Component,但交互只发生在 client):
import SearchBox from "./SearchBox";
export default async function PostsPage() {
// 仍然服务端取数
const posts = await fetchPosts();
return (
<main style={{ padding: 24 }}>
<h1>Posts</h1>
<SearchBox
onChange={(k) => {
// 演示:真实项目你可能用 router + searchParams
console.log("keyword:", k);
}}
/>
{/* ...列表... */}
</main>
);
}
你看,交互是交互,渲染是渲染,锅分得很清楚。
“清楚”这种东西,写项目时比“聪明”更重要。
5. 后端(NestJS):把“写得快”换成“活得久”
5.1 NestJS 的模块化:别嫌麻烦,它是在帮你躲未来的坑
Nest 官方文档体系强调 Module / Provider(依赖注入)等概念。
我们做一个 Posts 模块:
apps/api/src/posts/
posts.module.ts
posts.controller.ts
posts.service.ts
dto/
posts.module.ts
import { Module } from "@nestjs/common";
import { PostsController } from "./posts.controller";
import { PostsService } from "./posts.service";
@Module({
controllers: [PostsController],
providers: [PostsService],
})
export class PostsModule {}
posts.controller.ts
import { Controller, Get, Param, Query } from "@nestjs/common";
import { PostsService } from "./posts.service";
@Controller("posts")
export class PostsController {
constructor(private readonly posts: PostsService) {}
@Get()
async list(@Query("q") q?: string) {
return this.posts.list(q);
}
@Get(":id")
async detail(@Param("id") id: string) {
return this.posts.detail(id);
}
}
posts.service.ts
import { Injectable, NotFoundException } from "@nestjs/common";
type Post = {
id: string;
title: string;
content: string;
createdAt: string;
};
@Injectable()
export class PostsService {
private data: Post[] = [
{
id: "p1",
title: "我第一次把 RSC 用明白了",
content: "……(省略一万字情绪)",
createdAt: new Date().toISOString(),
},
{
id: "p2",
title: "写 NestJS 的人,最后都会开始爱上分层",
content: "……(真的,不骗你)",
createdAt: new Date().toISOString(),
},
];
async list(q?: string) {
if (!q) return this.data.map(this.toListItem);
const k = q.toLowerCase();
return this.data
.filter((p) => p.title.toLowerCase().includes(k) || p.content.toLowerCase().includes(k))
.map(this.toListItem);
}
async detail(id: string) {
const found = this.data.find((p) => p.id === id);
if (!found) throw new NotFoundException("Post not found");
return found;
}
private toListItem(p: Post) {
return {
id: p.id,
title: p.title,
excerpt: p.content.slice(0, 60) + (p.content.length > 60 ? "…" : ""),
createdAt: p.createdAt,
};
}
}
这只是“能跑”,离“能用”还差数据库、鉴权、校验、日志。
别急,我们一口口把它吃下去——但别噎着😅。
6. 数据库(PostgreSQL + Prisma):别让“数据”变成你最熟悉的陌生人
6.1 Prisma Schema:先把“内容”与“审计”建起来
Prisma Migrate 走的是 schema 驱动迁移。
apps/api/prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
password String
createdAt DateTime @default(now())
posts Post[]
}
model Post {
id String @id @default(cuid())
title String
content String
status PostStatus @default(DRAFT)
meta Json? // 给“可变字段”留口子(后面配 GIN)
authorId String
author User @relation(fields: [authorId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
logs AuditLog[]
}
model AuditLog {
id String @id @default(cuid())
action String
detail Json?
postId String
post Post @relation(fields: [postId], references: [id])
createdAt DateTime @default(now())
}
enum PostStatus {
DRAFT
PUBLISHED
}
迁移命令(示例):
npx prisma migrate dev --name init
到这一步,你项目的“骨架”终于开始长肉了。
但接下来有个问题会冒出来:
“meta 是 JSON,怎么查?怎么快?”
6.2 JSONB + GIN:让“灵活字段”不至于把性能搞成笑话
PostgreSQL 官方文档解释 GIN 索引擅长在复合值中检索元素。 ([PostgreSQL][5])
我们可以给 Post.meta 建 GIN(在 Prisma 里你可能用 migration SQL):
apps/api/prisma/migrations/.../migration.sql(示意)
-- Prisma 默认会建表,但 JSONB 的索引你最好显式补上
CREATE INDEX IF NOT EXISTS idx_post_meta_gin
ON "Post"
USING GIN ("meta");
然后你就能做类似查询:
- 查
meta里包含某个 tag - 查某个字段存在
- 查结构化条件(具体操作符与写法按你使用方式来)
我特别喜欢这套组合的原因很现实:
产品经理一句“我想加个字段”,你不需要连夜改表、迁移、回填、上线。
你只需要:meta 里加,并保持必要的索引策略。
7. 鉴权与会话:别把“登录”当成一行代码,它是半个安全体系
我知道你想跳过这章——毕竟“安全”最不讨喜,写起来枯燥,还容易挨骂。
但你想想:用户信息、内容后台、发布权限,哪个不是安全敏感点?
OWASP 的认证与会话管理建议非常务实,比如:认证强度、MFA、会话失效、超时、Cookie 安全属性等,都是你需要系统性考虑的点。
7.1 一个现实的折中:演示版用 JWT,生产版更推荐“短 token + 刷新机制 + 绑定策略”
先声明:
JWT 不是原罪,乱用才是。
“把 30 天有效期的 JWT 塞 localStorage”这种操作,属于“自信且危险”。
NestJS 示例:登录签发 token(简化)
import { Injectable, UnauthorizedException } from "@nestjs/common";
import * as jwt from "jsonwebtoken";
import * as crypto from "crypto";
@Injectable()
export class AuthService {
// 真实项目请用 bcrypt/argon2 之类做密码哈希,这里演示用
private hash(pwd: string) {
return crypto.createHash("sha256").update(pwd).digest("hex");
}
async login(email: string, password: string) {
const user = await this.findUserByEmail(email);
if (!user) throw new UnauthorizedException("Invalid credentials");
if (user.password !== this.hash(password)) {
throw new UnauthorizedException("Invalid credentials");
}
const token = jwt.sign(
{ sub: user.id, email: user.email },
process.env.JWT_SECRET!,
{ expiresIn: "15m" } // 让它短一点,别恋战
);
return { token };
}
private async findUserByEmail(email: string) {
// TODO: 用 Prisma 查库
return null as any;
}
}
7.2 会话管理:过期、失效、登出、绑定,都是“你迟早要补的债”
OWASP 会话管理指南提到会话应具备唯一性、不可猜测、失效机制与超时策略等要求。
你至少要做到:
- 短期访问 token(15m 这种)
- 刷新 token 存在 HttpOnly Cookie(降低 XSS 窃取风险)
- 服务端维护 refresh token 的撤销(黑名单或版本号)
- 登出时立刻失效
- Cookie 设置 Secure / SameSite 等属性(按你的站点策略选择)
我每次看到“token 永不过期”,心里都想给自己也发一个“永不过期的工资”。
但现实通常不允许,对吧🙂。
8. API 设计:别只顾“能用”,也得顾“好维护”
8.1 错误结构统一:让前端少猜谜
建议统一返回结构(示例):
{
"code": "POST_NOT_FOUND",
"message": "Post not found",
"requestId": "..."
}
并且服务端日志里带 requestId。
别小看这个——线上排查时它能救你命。
8.2 输入校验:别信“前端会帮我挡住”
你可以用 class-validator / zod 等。核心原则是:所有进入后端的数据都不可信。
(是的,包括你自己写的前端。)
9. Docker Compose:把“在我电脑上能跑”按在地上摩擦
Docker Compose 官方参考强调:Compose 用于定义多容器应用的服务、网络、卷等。
infra/docker/docker-compose.yml(示例)
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
POSTGRES_DB: creator_hub
ports:
- "5432:5432"
volumes:
- db_data:/var/lib/postgresql/data
api:
build:
context: ../../
dockerfile: infra/docker/api.Dockerfile
environment:
DATABASE_URL: postgres://postgres:postgres@db:5432/creator_hub
JWT_SECRET: "dev_secret_change_me"
ports:
- "4000:4000"
depends_on:
- db
web:
build:
context: ../../
dockerfile: infra/docker/web.Dockerfile
environment:
API_BASE_URL: http://api:4000
ports:
- "3000:3000"
depends_on:
- api
volumes:
db_data:
你会发现,Compose 一旦写顺了,团队协作体验会变得很“正常”。
正常到你会怀疑:**“以前我为啥要手动装一堆东西然后祈祷它们别打架?”**😵💫
10. CI(GitHub Actions):让质量靠流程,而不是靠祈祷
GitHub Actions 官方文档说明它能在仓库内自动化 CI/CD 工作流。
.github/workflows/ci.yml(示例)
name: ci
on:
push:
branches: ["main"]
pull_request:
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install
run: npm ci
- name: Lint
run: npm run lint
- name: Test
run: npm run test --if-present
- name: Build
run: npm run build
CI 这东西最讽刺的一点是:
你不配它,大家都觉得“没必要”;
你配了它,大家才开始觉得“幸好配了”。
——典型的“事后诸葛亮型价值”。
11. 性能与缓存:别等用户骂你“卡”,你才想起优化
11.1 Next.js 的渲染拆分:别一刀切
Next.js 文档讲得很明白:Server/Client Components 的边界决定了渲染与 hydration 的分配方式。 ([Next.js][2])
你可以把策略总结成一句话:
- 静态内容:尽量服务端渲染/缓存
- 强交互:局部 Client Component
- 列表页:分页、搜索、缓存要规划
- 详情页:按更新频率决定缓存策略
11.2 数据库索引:别“等慢了再加”,那时你加的不是索引,是眼泪
GIN 索引只是例子,真正要做的是:
- 查得最多的字段:BTree
- 文本搜索:tsvector + GIN/GiST
- JSONB:GIN
- 组合条件:复合索引
- 写多读少:别乱加索引(写放大很痛)
12. 工程习惯:看起来“啰嗦”,其实是在给未来的你写情书
我特别想把下面这几条钉在每个全栈项目的墙上:
- 写清楚环境变量清单(
.env.example) - 统一错误码(别让前端靠字符串判断)
- 请求日志 + requestId(线上问题 80% 靠它定位)
- 数据库迁移可追溯(别手动改生产库)
- 安全基线(密码存储、会话策略、Cookie 属性、CSRF/XSS 对策)
- CI 里跑 lint/test/build(别把质量寄托在“我检查过了”)
13. 结尾:你现在还觉得“全栈”只是把两端拼起来吗?
如果你看到这里,说明你至少愿意把“全栈”当一件正经事。
这比什么都重要。
全栈不是“我都会一点”,而是你能回答这些问题:
- 当页面白屏时,你能判断是构建、路由、渲染还是接口问题吗?
- 当接口变慢时,你能从日志、SQL、索引、缓存一路排查到根因吗?
- 当权限被绕过时,你能从会话、鉴权、边界条件找到漏洞在哪吗?
- 当需求改动时,你能让架构“改得动”,而不是“改不动就重写”吗?
所以我最后送你一句我自己写代码时常对自己说的话(带点情绪,别笑):
“别急着写酷的,先把对的写稳。”
酷是锦上添花,稳才是活下去的底气。
🧧福利赠与你🧧
无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学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)