开场白:为什么我的数据像前任一样失联?
“我明明
fetch('/api/users'),它却回我 404,像极了我给她发消息她已读不回。”
别哭,少年。把数据库想象成一位高冷的女神,Next 全栈就是帮你写情书的邮局。
只要信封(接口结构)写得够优雅,女神就会回你一句甜甜的 JSON。
1. 技术选型:女神的三种性格
| 性格 | 技术栈 | 恋爱暗号 |
|---|---|---|
| 活泼外向 🐬 | Prisma + PostgreSQL | 类型安全,自动生成 SQL |
| 高冷御姐 🧊 | Drizzle + PostgreSQL | 函数式 SQL,零运行时 |
| 神秘猫娘 🐱 | MongoDB + Mongoose | 文档型,模式灵活 |
本文以 Prisma + PostgreSQL 为例,因为她自带类型提示,像贴心女友帮你整理衣领。
2. 信封长啥样?——接口四层结构
┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐
│ 前端 │ │ API │ │ Service │ │ Database │
│ 情书正文 │----▶│ 邮局窗口 │----▶│ 红娘翻译 │----▶│ 女神回信 │
└────────────┘ └────────────┘ └────────────┘ └────────────┘
-
前端层(React Hook)
用 SWR 或 React-Query 当信鸽,自动重发、缓存。 -
API 路由层(
/pages/api或/app/api)
只做“收信”和“回信”,不处理业务。 -
Service 层
把情书翻译成 SQL,负责事务、权限、数据裁剪。 -
数据库
女神本人,只认 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 是必填 | 用 zod 或 Valibot 共享 schema |
| 连接池爆炸 💥 | PrismaClient 每次请求 new 一次 | 在全局单例,或用 globalThis.prisma |
| 跨域 403 🌉 | API 被外部域名调用 | Next 14 自带 CORS 白名单,或 nextjs-cors |
| 大字段查询 🐘 | 文章正文 1MB 也塞进列表 | 列表用 select: { id, title },详情再查 |
5. 进阶:把女神变成 GraphQL 女神 ❤️🔥
如果你喜欢“一次查询拿所有数据”的爽感,可以把 Prisma 套上 Nexus 或 Pothos,生成类型安全的 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。