全栈看板实战:NestJS + Prisma 后端 × Next.js + shadcn/ui 前端,Docker 一键部署

19 阅读6分钟

前端开发者想要快速转型为全栈开发,好奇后端技术框架是怎样? 数据库怎么设计?服务怎么部署?别急,我最近用 TypeScript 搓了个小项目,把全栈这条线捋了一遍,分享给你。

为啥选这个题目

全栈开发的难点不是某一项技术,而是理解多个环节如何协作:前端组件怎么调用后端 API、API 怎么操作数据库、数据库变更怎么通过迁移管理、整个应用怎么打包部署。

所以我做了fullstack-kanban 正是为解决这个痛点而设计的学习项目。它统一用 TypeScript 语言,覆盖了全栈开发的完整链路:

浏览器 (React 19)
  │  HTTP + JWT
  ▼
Next.js 16 ──── CORS ──── NestJS 11
                               │
                          Prisma ORM
                               │
                          PostgreSQL 16

一行 docker compose up --build 就能本地跑起来。下面逐一介绍每个技术点。


一、前端:Next.js 16 + React 19 + Tailwind CSS 4

Next.js App Router — 文件即路由

Next.js 16 的 App Router 让路由定义回归直觉——文件夹结构就是 URL 结构:

src/app/
├── page.tsx           → /           (登录页)
├── boards/
│   ├── page.tsx       → /boards     (看板列表)
│   └── [boardId]/
│       └── page.tsx   → /boards/123 (看板详情)

layout.tsx 作为根布局包裹所有页面,AuthProviderThemeProvider 在这里注入全局状态。

React 19 — 组件、Hooks 与状态管理

项目没有引入 Redux 或 Zustand,而是用 React 原生的 Context API 管理全局认证状态。这是一个适合入门的状态管理方案——先理解 useState / useEffect / useContext / useCallback 四个核心 Hook,再决定是否需要更重的方案。

认证逻辑就三步:

  1. createContext 创建上下文
  2. AuthProvider 包裹子组件,管理 token 和 user 状态
  3. useAuth() 自定义 Hook 暴露 login / logout / user

Tailwind CSS 4 — 原子化样式

Tailwind 4 用 @import "tailwindcss" 一行替代了旧版的配置文件,通过 @theme 指令定义主题变量。亮色/暗色切换只需修改 CSS 变量:

:root { --background: oklch(1 0 0); }
.dark { --background: oklch(0.12 0 0); }

shadcn/ui — 不是组件库的组件库

shadcn/ui 的核心理念:组件代码直接复制到你的项目里。没有额外的 npm 包依赖,你可以完全控制每一行代码。项目使用了 Button、Card、Dialog、AlertDialog 等 12 个组件,涵盖表单、弹窗、确认框、滚动区域、骨架屏等常见场景。

深入阅读Next.js/React 前端工程方… | shadcn/ui 使用说明


二、拖拽交互:从前端状态到后端事务

拖拽是本项目最复杂的交互,也是理解全栈协作的最佳切入点。

前端 — 乐观更新

拖拽采用 乐观更新 策略:先立即更新 UI,再后台发请求。失败时重新获取数据回滚。这给用户"即时响应"的体验。

架构分层清晰:

  • DragDropContext(Board 组件)— 协调整个拖拽过程
  • Droppable(List 组件)— 定义放置区域
  • Draggable(Card 组件)— 定义可拖拽元素

后端 — 位置重排事务

卡片拖动后,后端需要在数据库事务中调整位置。核心逻辑分两种情况:

同列表内移动:区间内的卡片位移 ±1,腾出目标位置。

移动前:[A:0] [B:1] [C:2] [D:3] [E:4]B(1) 移动到位置 3
第一步:position > 1 且 <= 3 的卡片前移    C(21), D(32)
第二步:Bposition 设为 3
结果:  [A:0] [C:1] [D:2] [B:3] [E:4]

跨列表移动:源列表补位 + 目标列表腾位,两步 updateMany 在一个 $transaction 中原子执行。

这些操作都在 Prisma 的交互式事务中完成——要么全部成功,要么全部回滚,保证数据一致性。

深入阅读卡片拖动调用栈分析 — 从用户松开鼠标到数据库 COMMIT 的完整链路。


三、后端:NestJS 11 模块化架构

如果你熟悉 Angular,NestJS 的设计会让你感到亲切。如果不熟悉也没关系——NestJS 的模块系统核心概念只有这几个:

概念职责
Module把相关的控制器和服务打包,是组织代码的基本单元
Controller处理 HTTP 请求,接收参数、返回响应,不写业务逻辑
Service存放业务逻辑,通过依赖注入被 Controller 调用
Guard在请求到达 Controller 之前拦截检查(如是否已登录)
Pipe对请求参数做转换和校验
DTO用类定义请求数据的结构和校验规则

每个功能模块(auth / boards / lists / cards)遵循相同的结构:*.module.ts + *.controller.ts + *.service.ts + dto/

请求的一生

GET /boards/1 为例:

HTTP 请求到达 ExpressCORS 中间件检查来源
  → ValidationPipe 校验参数
  → 路由匹配到 BoardsController.findOne()
  → JwtAuthGuard 验证 JWT,解析出 userId
  → ParseIntPipe"1" 转为数字
  → BoardsService.findOne(userId, 1)
  → Prisma 查询数据库 + 所有权检查
  → JSON 响应返回

装饰器声明式地完成了路由定义、参数提取、认证校验——这些都是后端开发的通用模式。

深入阅读NestJS 工程方案详解


四、认证:JWT + Passport

认证是前后端协作最紧密的环节之一,流程简洁清晰:

前端                          后端
POST /auth/login              │
{ username, password }  ────> │ bcrypt.compare() 验证密码
                              │ jwtService.sign() 签发 Token
                              │
{ access_token: "eyJ..." } <────
                              │
localStorage 存储 Token       │
                              │
GET /boards                   │
Authorization: Bearer eyJ... ──> JwtStrategy 验证签名和过期时间
                              │ req.user = { userId, username }
                              │
boards 数据 <────────────────── BoardsService.findAll(userId)

关键设计:

  • bcrypt 加盐哈希密码,数据库里存的是不可逆的哈希值
  • JWT 7 天有效期,Token 中携带 sub(用户 ID)和 username
  • 401 自动登出:前端的 lib/api.ts 拦截 401 响应,自动清除 token 并跳转登录页
  • DTO 校验class-validator 装饰器确保用户名至少 3 字符、密码至少 6 字符

五、数据库:PostgreSQL + Prisma ORM

关系建模

四张表形成清晰的一对多层级关系:

User  1──*  Board  1──*  List  1──*  Card

关键 Schema 设计决策:

  • 级联删除onDelete: Cascade)— 删除 Board 时自动删除其下所有 List 和 Card,避免孤儿数据
  • 复合唯一约束@@unique([boardId, position]))— 同一看板内列表位置不重复,同一列表内卡片位置不重复
  • Prisma 7 的 prisma-client provider — 新版生成器,输出到 generated/prisma/ 而非 node_modules

为什么用 ORM 而不是手写 SQL

// 手写 SQL — 容易拼错、没有类型提示
const result = await db.query("SELECT * FROM users WHERE username = $1", [username]);

// Prisma — 类型安全、自动补全、重构友好
const user = await prisma.user.findUnique({ where: { username } });

Prisma 的 include 语法替代了手写 JOIN:

prisma.board.findMany({
  where: { userId },
  include: {
    lists: {
      orderBy: { position: 'asc' },
      include: { cards: { orderBy: { position: 'asc' } } },
    },
  },
});
// 等价于多层 LEFT JOIN + ORDER BY

深入阅读PostgreSQL 工程方案详解


六、部署:Docker Compose 一键启动

全栈应用有前端、后端、数据库三个服务,手动配置环境变量和启动顺序很繁琐。Docker Compose 把这些全部自动化:

# docker-compose.yml 核心结构
services:
  postgres:      # 数据库 — 健康检查确保就绪
  backend:       # API — 等待 postgres 健康后启动,自动执行迁移
  frontend:      # Web — 等待 backend 健康后启动

关键工程实践:

  • 健康检查healthcheck)— 数据库用 pg_isready,后端用 wget 自检,确保依赖服务真正就绪
  • 多阶段构建AS builder / AS runner)— 构建阶段安装全部依赖并编译,运行阶段只拷贝产物,镜像体积从数百 MB 降到几十 MB
  • 依赖启动顺序depends_on + condition: service_healthy)— 保证 postgres 就绪后才启动 backend
  • 数据持久化(命名卷 postgres-data)— 容器删除重建后数据不丢失
  • 网络隔离 — 容器间通过服务名通信(@postgres:5432),宿主机通过映射端口访问(localhost:5433

深入阅读Docker 工程方案详解


动手实践

快速启动

git clone https://github.com/PeixuanLi/fullstack-kanban.git
cd fullstack-kanban
docker compose up --build

不用装 Node、不用配 PostgreSQL,有 Docker 就行。

建议的学习路径

  1. 跑起来,用起来 — 注册账号,创建看板,拖拽卡片,感受完整的用户流程
  2. 读前端代码 — 从 frontend/src/app/page.tsx 开始,跟着路由走一遍
  3. 读后端代码 — 从 backend/src/main.ts 开始,理解请求如何被处理
  4. 读数据库 Schema — 打开 backend/prisma/schema.prisma,理解表结构和关系
  5. 读拖拽调用栈 — 对照 card-drag-call-stack.md 理解一次拖拽的完整前后端链路
  6. 尝试修改 — 给卡片加个颜色标签、给看板加个描述字段,练习从前端到数据库的全链路开发

文档索引(按需取用)

文档内容
Next.js/React 前端工程方…App Router、React Hooks、状态管理、API 封装、拖拽实现、Tailwind CSS
shadcn/ui 使用说明组件库配置、已安装组件、添加新组件、主题定制
NestJS 工程方案详解模块架构、控制器、服务、DTO 校验、JWT 认证、Prisma 集成
PostgreSQL 工程方案详解数据建模、Prisma ORM、迁移管理、连接池、事务操作
Docker 工程方案详解Compose 编排、Dockerfile 多阶段构建、端口映射、数据卷
卡片拖动调用栈分析从鼠标松开到数据库 COMMIT 的完整调用链

写在最后

全栈开发没有想象中那么复杂。核心就三点:前端发请求 → 后端处理 + 查库 → 数据库存数据。每个环节都有成熟工具,关键是理解它们怎么协作。

这个项目我刻意控制复杂度:技术选型"刚好够用",不堆新东西;但该有的工程实践(鉴权、校验、事务、容器化)一个没少。毕竟学习项目也得贴近真实场景,不然练了也白练。

如果你看完觉得有点启发,欢迎:

  • GitHub 点个 Star,让更多人看到
  • Fork 一份,改成你自己的小项目练手
  • 或者直接提个 Issue,聊聊你的想法

有问题随时交流,代码和人,都欢迎来撩 😄