🏗️ 在 Next 全栈宇宙里与数据库谈恋爱

195 阅读3分钟

开场白:为什么我的数据像前任一样失联?

“我明明 fetch('/api/users'),它却回我 404,像极了我给她发消息她已读不回。”

别哭,少年。把数据库想象成一位高冷的女神,Next 全栈就是帮你写情书的邮局。
只要信封(接口结构)写得够优雅,女神就会回你一句甜甜的 JSON。


1. 技术选型:女神的三种性格

性格技术栈恋爱暗号
活泼外向 🐬Prisma + PostgreSQL类型安全,自动生成 SQL
高冷御姐 🧊Drizzle + PostgreSQL函数式 SQL,零运行时
神秘猫娘 🐱MongoDB + Mongoose文档型,模式灵活

本文以 Prisma + PostgreSQL 为例,因为她自带类型提示,像贴心女友帮你整理衣领。


2. 信封长啥样?——接口四层结构

┌────────────┐      ┌────────────┐      ┌────────────┐      ┌────────────┐
│   前端     │      │   API      │      │  Service   │      │  Database  │
│  情书正文   │----▶│  邮局窗口   │----▶│  红娘翻译   │----▶│  女神回信   │
└────────────┘      └────────────┘      └────────────┘      └────────────┘
  1. 前端层(React Hook)
    用 SWR 或 React-Query 当信鸽,自动重发、缓存。

  2. API 路由层/pages/api/app/api
    只做“收信”和“回信”,不处理业务。

  3. Service 层
    把情书翻译成 SQL,负责事务、权限、数据裁剪。

  4. 数据库
    女神本人,只认 SQL。


3. 实战:从 0 到牵手

3.1 数据库 schema —— 先画肖像

// prisma/schema.prisma
model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  createdAt DateTime @default(now())
  posts     Post[]
}

model Post {
  id       Int     @id @default(autoincrement())
  title    String
  content  String?
  published Boolean @default(false)
  author   User    @relation(fields: [authorId], references: [id])
  authorId Int
}

运行

npx prisma migrate dev --name init

女神说:“收到,肖像已存。”


3.2 Service 层 —— 红娘翻译官

// lib/service.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

export async function createUser(data: { email: string; name?: string }) {
  return prisma.user.create({ data });
}

export async function getUserWithPosts(userId: number) {
  return prisma.user.findUnique({
    where: { id: userId },
    include: { posts: true },
  });
}

注意:Prisma 返回的是 Promise,记得 await,否则你会收到女神的沉默。


3.3 API 路由 —— 邮局窗口

⛳ Pages Router 版

// pages/api/users.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { createUser } from '@/lib/service';

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method === 'POST') {
    const user = await createUser(req.body);
    return res.status(201).json(user);
  }
  res.setHeader('Allow', ['POST']);
  res.status(405).end(`Method ${req.method} Not Allowed`);
}

🌊 App Router 版(Route Handler)

// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createUser } from '@/lib/service';

export async function POST(request: NextRequest) {
  const body = await request.json();
  const user = await createUser(body);
  return NextResponse.json(user, { status: 201 });
}

3.4 前端 —— 信鸽与情书

// hooks/useCreateUser.ts
import useSWRMutation from 'swr/mutation';

async function sendRequest(url: string, { arg }: { arg: { email: string } }) {
  const res = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(arg),
  });
  if (!res.ok) throw new Error(await res.text());
  return res.json();
}

export function useCreateUser() {
  return useSWRMutation('/api/users', sendRequest);
}
// components/UserForm.tsx
'use client';
import { useCreateUser } from '@/hooks/useCreateUser';

export default function UserForm() {
  const { trigger, isMutating } = useCreateUser();

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    const email = formData.get('email') as string;
    await trigger({ email });
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" required placeholder="邮箱" />
      <button disabled={isMutating}>
        {isMutating ? '📮寄信中…' : '💌投递情书'}
      </button>
    </form>
  );
}

4. 防分手指南:常见坑位地图 🗺️

坑位症状解药
类型漂移 👻前端写 email?: string,后端 schema 是必填zodValibot 共享 schema
连接池爆炸 💥PrismaClient 每次请求 new 一次在全局单例,或用 globalThis.prisma
跨域 403 🌉API 被外部域名调用Next 14 自带 CORS 白名单,或 nextjs-cors
大字段查询 🐘文章正文 1MB 也塞进列表列表用 select: { id, title },详情再查

5. 进阶:把女神变成 GraphQL 女神 ❤️‍🔥

如果你喜欢“一次查询拿所有数据”的爽感,可以把 Prisma 套上 NexusPothos,生成类型安全的 GraphQL:

// graphql/schema.ts
import { objectType } from 'nexus';

export const User = objectType({
  name: 'User',
  definition(t) {
    t.int('id');
    t.string('email');
    t.list.field('posts', { type: 'Post' });
  },
});

前端直接:

query {
  user(id: 1) {
    email
    posts { title }
  }
}

6. ASCII 情书:一次完整的请求之旅

┌────────────┐     POST /api/users      ┌────────────┐
│  <form>    │ ───────────────────────▶ │  /api/users│
│            │  {email: 'a@b.com'}      │   route.ts │
└────┬───────┘                            └────┬───────┘
     │                                          │
     │  prisma.user.create()                    │
     │                                          ▼
┌────┴───────┐                            ┌────────────┐
│ 前端 SWR   │ ◀────── JSON user ─────────│ PostgreSQL │
└────────────┘                            └────────────┘

7. 彩蛋:一封真正的情书(Base64 版)

复制到控制台试试:

console.log(atob('WW91IGFyZSBteSBkYXRhYmFzZSBhbmQgSSBhbSB5b3VyIHNlcnZpY2UgbGF5ZXI='));

结语:恋爱是双向奔赴,数据也是

  • 前端负责颜值(UI)
  • API 负责礼仪(HTTP)
  • Service 负责翻译(SQL)
  • 数据库负责真心(ACID)

愿你与数据库白头偕老,永不 500。