{post.title}
{post.content}
作者:{post.author} | 发布于: {new Date(post.createdAt).toLocaleDateString('zh-CN')}学会用 MongoDB 做后端数据库,用 Next.js API Route 读写数据
作者:zyyc-0336 | 日期:2026-06-12
| 错误现象 | 原因 | 解决方案 |
|---|---|---|
MongooseError: buffering timed out | 连接字符串写错或网络不通 | 检查 .env.local 中的 MONGODB_URI,确认 IP 白名单 |
MongoServerError: bad auth | 用户名或密码错误 | 在 Atlas 面板重新设置数据库用户密码 |
ECONNREFUSED | 忘记启动 MongoDB 或连接地址错误 | Atlas 用户不需要本地启动,检查连接字符串 |
ValidationError: Path 'xxx' is required | 保存数据时缺少必填字段 | 检查 Schema 中的 required 设置 |
数据查出来是空数组 [] | 数据库中确实没有数据 | 先用代码插入几条测试数据 |
.env.local 不生效 | 文件名写错或忘记重启 | 确认文件名是 .env.local,修改后重启 npm run dev |
想象你开了一家奶茶店:
| 类比 | 对应技术 | 说明 |
|---|---|---|
| 奶茶店的账本 | 数据库 | 存放所有数据的地方 |
| 账本里的一页纸 | 集合(Collection) | 一组同类数据,比如「订单」 |
| 账本上的一行记录 | 文档(Document) | 一条具体数据,比如「张三点了一杯珍珠奶茶」 |
| 写账本的人 | 后端代码(API Route) | 负责读写数据库 |
没有数据库会怎样?
有了数据库:
| 对比项 | MongoDB | MySQL |
|---|---|---|
| 数据格式 | JSON 风格(灵活) | 表格(固定列) |
| 学习难度 | ⭐ 简单 | ⭐⭐ 中等 |
| 免费方案 | Atlas 免费 512MB | 需自建或购买 |
| 和 Next.js 配合 | 天然契合(都是 JSON) | 需要额外转换 |
| 适合场景 | 快速开发、原型、中小型项目 | 大型企业、复杂关系 |
一条文档长这样:
{
"_id": "6651a2b3c4d5e6f7a8b9c0d1",
"title": "我的第一篇文章",
"content": "Hello MongoDB!",
"author": "zyyc",
"createdAt": "2026-06-12T08:00:00.000Z"
}
MongoDB Atlas 是 MongoDB 官方提供的免费云数据库,不用在自己电脑上安装任何东西,注册就能用。
| 项目 | 说明 |
|---|---|
| 官网 | https://www.mongodb.com/atlas |
| 免费额度 | 512MB 存储,足够学习和小项目 |
| 注册方式 | 邮箱注册 / Google 账号一键登录 |
| 需要信用卡? | ❌ 不需要 |
第1步:打开 https://www.mongodb.com/atlas
第2步:点击 "Try Free" → 用邮箱注册
第3步:创建组织(Organization)→ 随便填名字
第4步:创建项目(Project)→ 随便填名字
第5步:创建集群(Cluster)→ 选 M0 FREE(免费的那个)
第6步:选择云服务商和地区 → 选 AWS + 新加坡(离中国近)
第7步:点击 "Create Cluster" → 等待 1-3 分钟创建完成
第1步:左侧菜单 → Security → Database Access
第2步:点击 "Add New Database User"
第3步:用户名和密码自己定(记住!后面要用)
第4步:权限选 "Read and write to any database"
第5步:点击 "Add User"
@、#、% 等特殊字符,否则连接字符串容易出错。建议用纯字母+数字。第1步:左侧菜单 → Security → Network Access
第2步:点击 "Add IP Address"
第3步:点击 "Allow Access from Anywhere"(会自动填 0.0.0.0/0)
第4步:点击 "Confirm"
第1步:回到 Clusters 页面
第2步:点击 "Connect" 按钮
第3步:选 "Drivers" → 选 "Node.js"
第4步:复制连接字符串,格式如下:
mongodb+srv://<用户名>:***@cluster0.xxxxx.mongodb.net/?retryWrites=true&w=majority
实际替换后:
mongodb+srv://zyyc:***@cluster0.abc123.mongodb.net/?retryWrites=true&w=majority
<用户名> 和 <密码> 要替换成你自己设置的,不要带 < >@ → %40)...mongodb.net/myblog?retryWrites=true&w=majority| 概念 | 说明 |
|---|---|
| MongoDB 驱动 | 底层库,直接操作数据库 |
| mongoose | 基于驱动的高级库,提供模型、验证等功能 |
| 类比 | MongoDB 驱动 = 手动挡,mongoose = 自动挡 |
npm install mongoose
在项目根目录创建 .env.local 文件:
MONGODB_URI=mongodb+srv://zyyc:***@cluster0.abc123.mongodb.net/myblog?retryWrites=true&w=majority
.env.local,不是 .env,不是 .env.local.txt.env.local 后必须重启 npm run dev.env.local 已在 .gitignore 中,不会被提交到 Git创建 lib/mongodb.js:
// lib/mongodb.js
import mongoose from 'mongoose'
const MONGODB_URI = process.env.MONGODB_URI
if (!MONGODB_URI) {
throw new Error('请在 .env.local 文件中设置 MONGODB_URI 环境变量')
}
// 用一个全局变量缓存连接,避免热重载时重复连接
let cached = global.mongoose
if (!cached) {
cached = global.mongoose = { conn: null, promise: null }
}
async function dbConnect() {
// 如果已经连接过了,直接返回
if (cached.conn) {
return cached.conn
}
// 如果正在连接中,等待它完成
if (!cached.promise) {
cached.promise = mongoose.connect(MONGODB_URI).then((mongoose) => {
return mongoose
})
}
cached.conn = await cached.promise
return cached.conn
}
export default dbConnect
| 概念 | 类比 | 作用 |
|---|---|---|
| Schema(模式) | 表格的列定义 | 描述数据长什么样 |
| Model(模型) | 表格本身 | 用来增删改查 |
| Document(文档) | 表格的一行 | 一条具体数据 |
关系:Schema 定义结构 → Model 基于 Schema 创建 → Document 是 Model 的实例
创建 models/Post.js:
// models/Post.js
import mongoose from 'mongoose'
// 定义 Schema:文章的数据结构
const PostSchema = new mongoose.Schema({
title: {
type: String, // 标题是字符串
required: [true, '标题不能为空'], // 必填
},
content: {
type: String, // 内容是字符串
required: [true, '内容不能为空'],
},
author: {
type: String, // 作者是字符串
default: '匿名', // 默认值
},
createdAt: {
type: Date, // 创建时间是日期
default: Date.now, // 默认当前时间
},
})
// 创建 Model 并导出
// mongoose.models.Post 防止热重载时重复创建模型
export default mongoose.models.Post || mongoose.model('Post', PostSchema)
| Mongoose 类型 | 说明 | 示例值 |
|---|---|---|
String | 字符串 | "Hello" |
Number | 数字 | 42 |
Boolean | 布尔值 | true |
Date | 日期 | new Date() |
Array | 数组 | [1, 2, 3] |
ObjectId | MongoDB 特殊 ID | "6651a2b3..." |
mongoose.models.Post || mongoose.model('Post', PostSchema) 这行代码必须有!否则 Next.js 热重载时会报 OverwriteModelError。| 操作 | 方法 | mongoose 语法 | 说明 |
|---|---|---|---|
| 创建 | POST | Model.create(data) | 插入一条数据 |
| 读取 | GET | Model.find(filter) | 查询数据 |
| 更新 | PUT | Model.updateOne(filter, data) | 修改数据 |
| 删除 | DELETE | Model.deleteOne(filter) | 删除数据 |
创建 app/api/posts/route.js:
// app/api/posts/route.js
import { NextResponse } from 'next/server'
import dbConnect from '@/lib/mongodb'
import Post from '@/models/Post'
// GET /api/posts → 获取所有文章
export async function GET() {
try {
// 1. 连接数据库
await dbConnect()
// 2. 查询所有文章,按创建时间倒序排列
const posts = await Post.find({}).sort({ createdAt: -1 })
// 3. 返回数据
return NextResponse.json(posts)
} catch (error) {
return NextResponse.json(
{ error: '获取文章失败:' + error.message },
{ status: 500 }
)
}
}
在同一个 app/api/posts/route.js 中添加:
// POST /api/posts → 创建新文章
export async function POST(request) {
try {
// 1. 连接数据库
await dbConnect()
// 2. 从请求体获取数据
const body = await request.json()
// 3. 创建文章
const post = await Post.create({
title: body.title,
content: body.content,
author: body.author,
})
// 4. 返回创建的文章(状态码 201 = 创建成功)
return NextResponse.json(post, { status: 201 })
} catch (error) {
return NextResponse.json(
{ error: '创建文章失败:' + error.message },
{ status: 500 }
)
}
}
| 方法 | 用途 | 示例 |
|---|---|---|
find({}) | 查所有 | Post.find({}) |
find({ author: 'zyyc' }) | 按条件查 | Post.find({ author: 'zyyc' }) |
findById(id) | 按 ID 查一条 | Post.findById('6651a2b3...') |
findOne({ title: 'xxx' }) | 查第一条匹配的 | Post.findOne({ title: '标题' }) |
countDocuments({}) | 统计数量 | Post.countDocuments({}) |
limit(10) | 限制返回条数 | Post.find({}).limit(10) |
skip(5) | 跳过前 N 条(分页用) | Post.find({}).skip(5).limit(10) |
创建 app/posts/page.js:
// app/posts/page.js
'use client'
import { useState, useEffect } from 'react'
export default function PostsPage() {
const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(true)
// 页面加载时从 API 获取文章
useEffect(() => {
fetch('/api/posts')
.then((res) => res.json())
.then((data) => {
setPosts(data)
setLoading(false)
})
.catch((err) => {
console.error('获取文章失败:', err)
setLoading(false)
})
}, [])
if (loading) {
return 加载中...
}
return (
📚 文章列表(来自 MongoDB)
{posts.length === 0 ? (
暂无文章,快去 /api/posts 接口用 POST 创建一篇吧!
) : (
posts.map((post) => (
{post.title}
{post.content}
作者:{post.author} | 发布于:
{new Date(post.createdAt).toLocaleDateString('zh-CN')}
))
)}
)
}
_id(带下划线),不是 id!用 post.id 会得到 undefined。# 第1步:创建项目
npx create-next-app@latest my-blog
cd my-blog
# 第2步:安装 mongoose
npm install mongoose
# 第3步:创建 .env.local
# 在项目根目录新建 .env.local,写入连接字符串
# 第4步:创建文件
# lib/mongodb.js → 数据库连接
# models/Post.js → 文章模型
# app/api/posts/route.js → API 接口
# app/posts/page.js → 前端页面
# 第5步:启动项目
npm run dev
# 第6步:测试
# 1) 访问 http://localhost:3000/posts → 应该显示空列表
# 2) 用 POST 请求创建文章
# 3) 刷新页面 → 应该显示文章了
创建文章:
curl -X POST http://localhost:3000/api/posts \
-H "Content-Type: application/json" \
-d '{"title":"Hello MongoDB","content":"这是我的第一篇数据库文章!","author":"zyyc"}'
查看文章:
curl http://localhost:3000/api/posts
| 场景 | 方法 |
|---|---|
| 查看数据库里有什么数据 | Atlas 面板 → Clusters → Browse Collections |
| 测试 API 接口 | 浏览器直接访问 localhost:3000/api/posts(GET) |
| 发送 POST 请求 | 用 curl / Postman / Thunder Client(VS Code 插件) |
| 查看服务器日志 | 终端里 npm run dev 的输出 |
| 连接失败排查 | 检查 IP 白名单 → 检查用户名密码 → 检查连接字符串格式 |
| 知识点 | 说明 |
|---|---|
| MongoDB Atlas | 免费云数据库,注册即用 |
| 连接字符串 | mongodb+srv://user:***@host/db |
.env.local | 存放敏感配置,不提交到 Git |
| mongoose | MongoDB 的 Node.js 驱动库 |
| Schema | 定义数据结构(字段、类型、校验) |
| Model | 基于 Schema 创建,用于增删改查 |
dbConnect() | 连接数据库(带缓存优化) |
Model.find() | 查询数据 |
Model.create() | 创建数据 |
| API Route 调用数据库 | 在 route.js 中连接数据库、执行操作 |
| 前端 fetch 调用 API | 用 fetch('/api/posts') 获取数据 |
| # | 错误 | 正确做法 |
|---|---|---|
| 1 | .env.local 文件名写错 | 必须是 .env.local,注意有个点 |
| 2 | 连接字符串中 <密码> 没替换 | 把 <> 和里面的内容都换成真实值 |
| 3 | 忘记 dbConnect() | 每个 API Route 都要先调用 await dbConnect() |
| 4 | 用 post.id 取 ID | MongoDB 的 ID 叫 _id(带下划线) |
| 5 | 忘记加 mongoose.models.Post || | 热重载会报错,必须防重复创建模型 |
下一章预告(第23章):我们将学习博客评论功能实现——从数据结构设计到前后端完整联调,给每篇文章加上评论区,让读者可以互动交流。
本文档由 zyyc-0336 编写,最后更新:2026-06-14
| 知识点 | 说明 |
|---|---|
| MongoDB Atlas | 免费云数据库,注册即用 |
| 连接字符串 | mongodb+srv://user:***@host/db |
.env.local | 存放敏感配置,不提交到 Git |
| mongoose | MongoDB 的 Node.js 驱动库 |
| Schema | 定义数据结构(字段、类型、校验) |
| Model | 基于 Schema 创建,用于增删改查 |
dbConnect() | 连接数据库(带缓存优化) |
Model.find() | 查询数据 |
Model.create() | 创建数据 |
| API Route 调用数据库 | 在 route.js 中连接数据库、执行操作 |
| 前端 fetch 调用 API | 用 fetch('/api/posts') 获取数据 |
下一章预告(第23章):我们将学习博客评论功能实现——从数据结构设计到前后端完整联调,给每篇文章加上评论区,让读者可以互动交流。
本文档由 zyyc-0336 编写,最后更新:2026-06-14