【Next.js】用 TaoToken 统一 Key 打通 CRUD API 与数据库交互(PostgreSQL + Prisma ORM 和 MongoDB + mongoose 两种方案)
1. 从本地能跑到线上能部署Next.js CRUD API 到底卡在哪Next.js 全栈 CRUD API 指的是在同一个工程里既写页面又写接口用app/api目录下的route.ts处理增删改查再通过 ORM 或 ODM 跟数据库交互。它能做什么一句话前端fetch(/api/products)就能拿到数据不用再单独起一个后端服务。适合谁适合已经会用 React、想一个人把全栈链路跑通的前端开发者或者小团队里需要快速交付可部署接口的人。但真正动手时卡点往往不在“怎么写一个 GET”而在几个具体的地方路由到底放app还是pages、模型定义和迁移怎么配合、数据库连接在开发热更新时会不会爆炸、本地.env和线上环境变量怎么对齐。更现实的一个问题是当项目里开始接入模型调用比如做 AI 商品描述生成、智能客服API Key 散落在各个文件里本地一套、线上又一套改一次要翻半天。我试过把模型调用的凭据统一收口到 TaoToken 的 Key 上配合数据库读写一起管理链路会清爽很多。这篇就按 PostgreSQL Prisma ORM 和 MongoDB mongoose 两套方案对照着写每一步都给可复制的配置和 curl 验证动作目标是一次跑通增删改查和数据库读写并且把模型调用凭据也纳入同一套管理。先说清楚两套方案的定位差异避免你选错维度PostgreSQL PrismaMongoDB mongoose数据形态关系型强 schema文档型灵活 schema迁移方式prisma migrate生成 SQL无迁移schema 在代码里类型安全Prisma 自动生成类型手写 interface适合场景订单、库存等强一致业务内容、日志等结构多变业务连接复用全局单例 PrismaClient全局缓存 mongoose 连接选型建议很直接如果你的字段经常变、嵌套结构多选 mongoose如果要做关联查询、事务、严格约束选 Prisma。下面两套都从零写一遍你可以只跟其中一套。2. TaoToken 前置准备统一 Key 与 API 通道管理模型调用凭据在写 CRUD 之前先把模型调用的凭据问题解决掉。很多人的做法是把 OpenAI 或别的 Key 直接写进.env然后在route.ts里process.env.OPENAI_API_KEY。这在单机 demo 里没问题但一旦要部署、要换模型、要在多个环境切换就会变成一团乱麻。TaoToken 在这里的角色是统一 Key 和 API 通道你只需要在 TaoToken 侧管理一份凭据项目里通过一个 Base URL 加一个 Key 去调用模型切换、额度查看、Key 轮换都在控制台完成代码里不用改。官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 这个不加 UTM。具体要准备三样东西也就是常说的“三件套”Base URLhttps://taotoken.net/apiAPI Key在控制台创建形如sk-xxxxModel ID比如gpt-4o-mini、claude-3-5-sonnet之类按你实际要用的填创建 Key 的入口在控制台的 API Keys 页面https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。如果你只是想先验证模型能不能通可以直接用模型对话页面试https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 。长期做编码或 Agent 的话Coding Plan 更划算https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。为什么要在 CRUD 项目里提前做这件事因为一个真实的商品接口POST 创建时很可能要调用模型生成描述或分类建议。如果你把模型调用和数据库写入放在同一个route.ts里凭据管理就必须和数据库连接一样可靠。统一到 TaoToken 后.env里只需要两个变量# .env.local TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_API_KEYsk-你的key TAOTOKEN_MODELgpt-4o-mini注意这里不要硬编码进代码.env.local要进.gitignore。线上部署时比如 Vercel在项目的 Environment Variables 里把这三个变量配上本地和线上就对齐了。这一步做完后面无论 Prisma 还是 mongoose模型调用都走同一套配置。如果你用的是 Claude Code 这类工具做辅助开发接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有 Base URL 和 Key 的配置说明照着填即可。这里不展开工具本身的安装只强调一点Base URL、Key、Model ID 三件套必须同时正确缺一个都会报错。3. 可复制配置Prisma schema 与 mongoose model 片段这一节给两套方案的核心配置都是可以直接复制进项目的。先看 PostgreSQL Prisma。项目初始化如果你已有 Next.js 工程直接在根目录装依赖npm install prisma prisma/client pg npx prisma init这会生成prisma/schema.prisma和.env。把数据库连接写进.envDATABASE_URLpostgresql://your_username:your_passwordlocalhost:5432/nextjs_db?schemapublic然后定义模型prisma/schema.prismagenerator client { provider prisma-client-js } datasource db { provider postgresql url env(DATABASE_URL) } model Product { id Int id default(autoincrement()) name String description String? price Decimal default(0) stock Int default(0) category String isActive Boolean default(true) createdAt DateTime default(now()) updatedAt DateTime updatedAt }跑迁移npx prisma migrate dev --name init连接复用是关键lib/prisma.tsimport { PrismaClient } from prisma/client declare global { var prisma: PrismaClient | undefined } const prisma global.prisma || new PrismaClient() if (process.env.NODE_ENV development) { global.prisma prisma } export default prisma这段的作用是开发环境下热更新不会反复 new 客户端避免连接数暴涨。再看 MongoDB mongoose。装依赖npm install mongoose mongodb.env.localMONGODB_URImongodb://localhost:27017/yourdbname模型定义models/Product.tsimport mongoose, { Document, Model, Schema } from mongoose export interface IProduct extends Document { name: string price: number description: string category: string stock: number featured?: boolean createdAt: Date updatedAt: Date } const productSchema: Schema new mongoose.Schema( { name: { type: String, required: [true, Please enter product name], trim: true, maxLength: [100, Product name cannot exceed 100 characters] }, price: { type: Number, required: [true, Please enter product price], default: 0.0 }, description: { type: String, required: [true, Please enter product description] }, category: { type: String, required: [true, Please select category for this product], enum: { values: [Electronics, Cameras, Laptops, Accessories, Books], message: Please select correct category for product } }, stock: { type: Number, required: [true, Please enter product stock], default: 0 }, featured: { type: Boolean, default: false } }, { timestamps: true } ) const Product: ModelIProduct mongoose.models.Product || mongoose.model(Product, productSchema) export default Product连接复用lib/dbConnect.tsimport mongoose from mongoose const MONGODB_URI process.env.MONGODB_URI if (!MONGODB_URI) { throw new Error(Please define the MONGODB_URI environment variable inside .env.local) } let cached (global as any).mongoose if (!cached) { cached (global as any).mongoose { conn: null, promise: null } } async function dbConnect() { if (cached.conn) { return cached.conn } if (!cached.promise) { const opts { bufferCommands: false } cached.promise mongoose.connect(MONGODB_URI, opts).then((m) m) } try { cached.conn await cached.promise } catch (e) { cached.promise null throw e } return cached.conn } export default dbConnect两套配置的共同点是都用全局变量缓存连接区别是 Prisma 缓存的是客户端实例mongoose 缓存的是连接对象。这一步做对后面 API 才不会在并发时崩。4. 验证请求curl 跑通增删改查与成功结果配置写完必须用 curl 验证别急着写前端。先启动npm run devPrisma 方案的列表接口app/api/products/route.ts核心逻辑import prisma from /lib/prisma import { NextResponse } from next/server export async function GET(request: Request) { try { const { searchParams } new URL(request.url) const category searchParams.get(category) const products await prisma.product.findMany({ where: { ...(category { category }) }, orderBy: { createdAt: desc } }) return NextResponse.json(products) } catch (error) { return NextResponse.json({ error: Failed to fetch products }, { status: 500 }) } } export async function POST(request: Request) { try { const body await request.json() if (!body.name || !body.category) { return NextResponse.json({ error: Name and category are required }, { status: 400 }) } const product await prisma.product.create({ data: { name: body.name, description: body.description || null, price: parseFloat(body.price) || 0, stock: parseInt(body.stock) || 0, category: body.category, isActive: body.isActive ! undefined ? body.isActive : true } }) return NextResponse.json(product, { status: 201 }) } catch (error) { return NextResponse.json({ error: Failed to create product }, { status: 500 }) } }单个产品操作app/api/products/[id]/route.ts用findUnique、update、delete注意id要parseInt。现在验证。创建curl -X POST http://localhost:3000/api/products \ -H Content-Type: application/json \ -d {name:New Product,price:99.99,description:Test product,category:Electronics,stock:10}成功返回 201 和带id的 JSON。列表curl http://localhost:3000/api/products返回数组。单个curl http://localhost:3000/api/products/1更新curl -X PUT http://localhost:3000/api/products/1 \ -H Content-Type: application/json \ -d {price:109.99}删除curl -X DELETE http://localhost:3000/api/products/1mongoose 方案的接口结构一样只是把prisma.product.findMany换成Product.find(query)POST 用Product.create(body)单个操作用findById、findByIdAndUpdate、findByIdAndDelete。mongoose 的列表接口建议加分页const page parseInt(searchParams.get(page) || 1) const limit parseInt(searchParams.get(limit) || 10) const products await Product.find(query) .limit(limit) .skip((page - 1) * limit) .exec() const count await Product.countDocuments(query) return NextResponse.json({ success: true, data: products, pagination: { page, limit, totalPages: Math.ceil(count / limit), totalItems: count } })curl 验证时如果 POST 返回 400先检查Content-Type有没有写对如果返回 500看终端日志里的数据库报错。两套方案都跑通后你的 CRUD 链路就成立了。5. 本篇常见错排查401、local proxy failed 与 reading choices这一节列真实会遇到的报错对照着查。401 Unauthorized模型调用时最常见。原因通常是TAOTOKEN_API_KEY没配、配错或者请求头里没带Authorization: Bearer sk-xxx。检查.env.local是否被 Next.js 读取改完要重启 dev server线上检查环境变量是否生效。如果用的是 Claude Code 或 Cline 这类工具确认 Base URL 填的是https://taotoken.net/apiKey 和 Model ID 三件套齐全。local proxy failed这个报错一般出现在本地请求发不出去或者工具配置了本地代理但代理没起来。先确认你的网络请求是直连https://taotoken.net/api不要配任何本地转发。如果工具里有 proxy 相关配置项清空它。同时检查防火墙有没有拦 443 端口。reading choices典型报错是Cannot read properties of undefined (reading choices)。这说明返回体里没有choices字段通常是请求失败但代码没判断状态码就直接取data.choices[0]。正确做法是先判断response.ok再解析。比如const res await fetch(${process.env.TAOTOKEN_BASE_URL}/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${process.env.TAOTOKEN_API_KEY} }, body: JSON.stringify({ model: process.env.TAOTOKEN_MODEL, messages: [{ role: user, content: 生成一段商品描述 }] }) }) if (!res.ok) { const errText await res.text() throw new Error(Model call failed: ${res.status} ${errText}) } const data await res.json() const content data.choices?.[0]?.message?.content ?? OAuth 相关报错如果你用 Codex 或类似工具auth.json里的配置要写全。Base URL、Key、Model ID 三件套缺一不可。auth.json示例{ baseURL: https://taotoken.net/api, apiKey: sk-你的key, model: gpt-4o-mini }Prisma 报 P1001 连不上数据库检查DATABASE_URL里的用户名、密码、端口、库名PostgreSQL 默认 5432。本地没起服务的话先pg_ctl start或用 Docker 起一个。mongoose 报 buffering timed out说明连接没建立就执行了查询。确认await dbConnect()在每个 handler 开头都调用了且MONGODB_URI正确。热更新后连接数暴涨Prisma 和 mongoose 都要用全局缓存前面lib/prisma.ts和lib/dbConnect.ts就是干这个的别省。排查顺序建议先看终端日志的完整报错再看状态码最后看环境变量。90% 的问题出在 Key 或连接串上。6. 把模型调用接进 CRUD统一 Key 后的收尾动作最后一步把模型调用真正接进你的 CRUD 流程验证统一 Key 的价值。比如在 POST 创建商品时如果description为空就调用模型生成一段export async function POST(request: Request) { try { const body await request.json() let description body.description if (!description) { const res await fetch(${process.env.TAOTOKEN_BASE_URL}/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${process.env.TAOTOKEN_API_KEY} }, body: JSON.stringify({ model: process.env.TAOTOKEN_MODEL, messages: [ { role: user, content: 为商品「${body.name}」写一句 30 字以内的卖点描述 } ] }) }) if (res.ok) { const data await res.json() description data.choices?.[0]?.message?.content ?? null } } const product await prisma.product.create({ data: { name: body.name, description, price: parseFloat(body.price) || 0, stock: parseInt(body.stock) || 0, category: body.category } }) return NextResponse.json(product, { status: 201 }) } catch (error) { return NextResponse.json({ error: Failed to create product }, { status: 500 }) } }这样数据库写入和模型调用在同一个接口里完成而凭据只有一份来自 TaoToken。部署到线上时把TAOTOKEN_BASE_URL、TAOTOKEN_API_KEY、TAOTOKEN_MODEL和DATABASE_URL或MONGODB_URI一起配到环境变量里本地和线上行为一致。如果你后面要长期做编码或 Agent 类项目建议把 Key 管理固定下来用 Coding Plan 的入口统一查看额度https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。需要新建或轮换 Key 时去 API Keys 页面https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。配置细节查文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。到这里两套数据库方案的 CRUD 都跑通了模型调用也接进来了凭据统一在 TaoToken 管理。接下来你可以把前端页面接上或者加一层中间件做鉴权。真正容易出问题的从来不是 CRUD 本身而是连接复用和凭据散落这两点处理干净从本地到部署基本不会翻车。