🎬《Next 全栈 CRUD 的百老汇》

0️⃣ 开场彩蛋:一张图先声夺人

没图?那就画一张 ASCII 海报!

vbnet 复制代码
┌─────────────────────────────┐
│      Next.js Full Stack     │
│         CRUD Musical        │
│                             │
│   🎤 Next   💃 Prisma   🥁 Pg │
└─────────────────────────────┘

1️⃣ 舞台搭建:项目初始化

角色 作用 安装命令
Next.js 13 App Router 前端 + API npx create-next-app@latest next-prisma-crud --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
Prisma ORM 数据舞者 npm i prisma @prisma/client
PostgreSQL 幕后仓库 Docker 一行起: docker run --name pg -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres:15

小贴士:

postgres 密码设成 postgres,就像把家门钥匙放在门垫下------方便但别上生产 🏠


2️⃣ 数据库建模:把剧本写成 schema

文件 prisma/schema.prisma

prisma 复制代码
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String
  published Boolean  @default(false)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

小图标:📜 "Prisma Schema:把 DDL 写成十四行诗"


3️⃣ 环境变量:别让密码裸奔

.env

ini 复制代码
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres?schema=public"

.env 加入 .gitignore,否则 GitHub 机器人会私信你:

"兄弟,密码走光了。"


4️⃣ 一键迁移:让数据库学会新舞步

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

这两行命令做了什么?

  1. migrate:把 schema 翻译成 SQL,像同声传译。
  2. generate:给 Prisma Client 生成 TypeScript 类型,像给演员发剧本。

5️⃣ 演员上场:Prisma Client 的单例模式

src/lib/prisma.ts

ts 复制代码
import { PrismaClient } from '@prisma/client';

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

export const prisma = globalForPrisma.prisma ?? new PrismaClient();

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

小图标:🎭 "单例模式:确保全场只有一位女主角"


6️⃣ 全栈 CRUD:一场四幕歌剧

🎼 第 1 幕:CREATE --- 新生

src/app/api/posts/route.ts

ts 复制代码
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';

export async function POST(req: NextRequest) {
  const { title, content } = await req.json();
  const post = await prisma.post.create({ data: { title, content } });
  return NextResponse.json(post, { status: 201 });
}

前端 src/app/new/page.tsx

tsx 复制代码
'use client';
import { useRouter } from 'next/navigation';

export default function NewPost() {
  const router = useRouter();
  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const form = new FormData(e.currentTarget);
    await fetch('/api/posts', {
      method: 'POST',
      body: JSON.stringify({
        title: form.get('title'),
        content: form.get('content'),
      }),
    });
    router.push('/');
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-4 p-8">
      <input name="title" placeholder="标题" className="border rounded w-full" />
      <textarea name="content" placeholder="内容" className="border rounded w-full h-32" />
      <button className="bg-blue-600 text-white px-4 py-2 rounded">发布</button>
    </form>
  );
}

观众反馈:🎉 "创建成功,数据库鼓掌 1 次"


🎼 第 2 幕:READ --- 回眸

src/app/api/posts/route.ts(GET)

ts 复制代码
export async function GET() {
  const posts = await prisma.post.findMany({ orderBy: { createdAt: 'desc' } });
  return NextResponse.json(posts);
}

src/app/page.tsx

tsx 复制代码
export default async function Home() {
  const posts: Post[] = await (await fetch('http://localhost:3000/api/posts', { cache: 'no-store' })).json();
  return (
    <ul>
      {posts.map(p => (
        <li key={p.id} className="border-b p-4">
          <h2 className="text-xl font-bold">{p.title}</h2>
          <p>{p.content}</p>
        </li>
      ))}
    </ul>
  );
}

小图标:👀 "服务端组件:SSR 免费,不用 Vercel 也请客"


🎼 第 3 幕:UPDATE --- 易容

src/app/api/posts/[id]/route.ts

ts 复制代码
export async function PUT(req: NextRequest, { params }: { params: { id: string } }) {
  const data = await req.json();
  const post = await prisma.post.update({
    where: { id: Number(params.id) },
    data,
  });
  return NextResponse.json(post);
}

前端加一个编辑按钮,路由 src/app/edit/[id]/page.tsx,复用 NewPost 组件并预填表单即可。

观众反馈:✍️ "改完标题,数据库说:我年轻了 5 岁"


🎼 第 4 幕:DELETE --- 谢幕

src/app/api/posts/[id]/route.ts

ts 复制代码
export async function DELETE(_: NextRequest, { params }: { params: { id: string } }) {
  await prisma.post.delete({ where: { id: Number(params.id) } });
  return NextResponse.json({ ok: true });
}

前端点击"删除"按钮触发:

ts 复制代码
await fetch(`/api/posts/${id}`, { method: 'DELETE' });

小图标:🗑️ "数据谢幕,观众请保持安静 404"


7️⃣ 彩蛋:实时订阅,让数据库开麦

Prisma 的实时功能(预览):

ts 复制代码
const stream = prisma.post.subscribe(); // 伪代码

下一季音乐剧:WebSocket + Server Sent Events + Prisma Pulse,敬请期待 🍿


8️⃣ 谢幕清单:部署走你

  1. 推代码到 GitHub

  2. 一键 Vercel:选 Postgres 模板,自动注入 DATABASE_URL

  3. 看日志:

    bash 复制代码
    [POST] /api/posts 201 23ms
    [GET]  /api/posts 200 15ms

观众鼓掌:👏 "从本地 3000 到线上 HTTPS,只花了 30 秒"


9️⃣ 彩蛋台词墙

角色 经典台词
Prisma Client "我不是 ORM,我是类型安全的舞伴。"
Next.js "App Router:服务端组件,免费 SSR,不加价。"
PostgreSQL "索引就像高跟鞋,漂亮但别乱穿。"

"愿你每次 npx prisma migrate 都能顺利通过,

愿每个 404 都变成 200 OK。"

------ 一名把 SQL 跳成芭蕾舞的全栈工程师 🩰

相关推荐
pany15 分钟前
体验一款编程友好的显示器
前端·后端·程序员
Zuckjet20 分钟前
从零到百万:Notion如何用CRDT征服离线协作的终极挑战?
前端
Java水解21 分钟前
深度剖析【Spring】事务:万字详解,彻底掌握传播机制与事务原理
后端·spring
ikonan25 分钟前
译:Chrome DevTools 实用技巧和窍门清单
前端·javascript
Juchecar25 分钟前
Vue3 v-if、v-show、v-for 详解及示例
前端·vue.js
开始学java26 分钟前
继承树追溯
后端
ccc101829 分钟前
通过学长的分享,我学到了
前端
编辑胜编程29 分钟前
记录MCP开发表单
前端
可爱生存报告29 分钟前
vue3 vite quill-image-resize-module打包报错 Cannot set properties of undefined
前端·vite
__lll_29 分钟前
前端性能优化:Vue + Vite 全链路性能提升与打包体积压缩指南
前端·性能优化