🎬《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 跳成芭蕾舞的全栈工程师 🩰

相关推荐
gis开发之家2 分钟前
《Vue3 从入门到大神50篇》Vue3 源码详解(二十):生命周期钩子源码解析 —— onMounted / onUpdated 如何实现?
前端·javascript·前端框架·vue3·vue3源码
zzzll11114 分钟前
Spring Boot 入门指南:从零开始构建微服务
spring boot·后端·微服务
Narrastory5 分钟前
我用 Claude Code 写代码不到 2 小时,却花了 3 天做完这个软件—Vibe Coding 的正确姿势,是设计不是生成
前端·人工智能·github
Enaium6 分钟前
我实现了KMP的SDL绑定并编写了高性能软光追
前端·kotlin
古夕11 分钟前
本地正常线上白屏:一次路由切换后刷新恢复问题的排查与修复
前端·ai编程
栀鸢ouo17 分钟前
解决Element Plus表格展开行横向溢出、滚动截断问题(项目实战方案)
前端·vue.js
用户390513321928821 分钟前
写了5年JS,才发现这10个方法能少写一半代码
前端
平凡的阿泽22 分钟前
我用TRAE Work手搓了一个「谁是卧底」小游戏
前端·javascript
运维行者_24 分钟前
企业带宽监控工具实战:网络流量分析与异常排查的5个关键能力
运维·服务器·开发语言·网络·分布式·后端·php
一心只读圣贤书30 分钟前
AI 辅助前端视觉回归治理:从截图基线到变更解释
前端·人工智能