使用 Express 和 Mongoose 构建 REST API
这篇文章最初发表在这里
本教程将指导您使用 Node.js、Express 和 Mongoose 构建具有 CRUD 功能的 RESTful API。我希望您具备 Node.js 和 JavaScript 的基础知识。如果您具备这些知识,那么您就可以开始了!
先决条件
首先需要在您的机器上安装这些软件:
入门
启动这个项目,我们唯一需要的就是一个已经初始化了 npm 包的空白文件夹。那就让我们创建一个吧!
$ mkdir learn-express
$ cd learn-express
$ npm init -y
现在,让我们安装一些有用的包。
$ npm install express mongoose body-parser
在这里,我们安装Express作为我们的 Web 框架,安装 mongoose与我们的 MongoDB 数据库交互,安装body-parser来解析我们的请求主体。
我还在我的 GitHub 上发布了整个项目的源代码。请将其克隆到您的计算机中。
$ git clone https://github.com/rahmanfadhil/learn-express-mongoose
基本 Express 服务器
我们现在可以开始创建index.js
并建立一个简单的 Express 服务器。
const express = require("express")
const app = express()
app.listen(5000, () => {
console.log("Server has started!")
})
我们首先导入express
刚刚安装的包。然后,创建一个新的 express 实例并将其放入app
变量中。这个app
变量让我们可以完成配置 REST API 所需的一切,例如注册路由、安装必要的中间件等等。
尝试通过运行以下命令来运行我们的服务器。
$ node index.js
Server has started!
或者,我们可以设置一个新的 npm 脚本来使我们的工作流程更加简单。
{
"scripts": {
"start": "node index.js"
}
}
然后,我们可以通过执行来运行我们的服务器npm start
。
$ npm start
Server has started!
设置 Mongoose
Mongoose 是 Node.js 最受欢迎的 MongoDB 包装器。它使我们能够轻松地与 MongoDB 数据库进行交互。我们可以开始将服务器连接到 MongoDB 数据库了。
const express = require("express")
const mongoose = require("mongoose") // new
// Connect to MongoDB database
mongoose
.connect("mongodb://localhost:27017/acmedb", { useNewUrlParser: true })
.then(() => {
const app = express()
app.listen(5000, () => {
console.log("Server has started!")
})
})
这里,我们导入了mongoose
包,并用它来连接到名为 的数据库acmedb
,不过你可以随意命名。如果你还没有创建该数据库,不用担心,mongoose 会帮你创建。
connect 方法返回一个承诺,因此我们可以等到它解决,然后运行我们的 Express 服务器。
再次运行服务器,确保没有错误。
$ npm start
Server has started!
现在,我们已经成功将服务器与数据库连接起来,现在是时候创建我们的第一个模型了。
猫鼬模型
在 NoSQL 世界中,每个数据都存储在一个文档中。多个相同类型的文档可以放在一个集合中。
模型是一个类,它让我们与数据库的特定集合进行交互。
定义模型还需要定义模式。模式基本上告诉模型我们的文档应该是什么样子。即使在 NoSQL 世界中,文档模式非常灵活,但 Mongoose 可以帮助我们保持数据更加一致。
假设我们有一个博客 API。那么,我们显然会创建一个Post
模型。这个帖子模型有一个 schema,其中包含可以添加到单个文档中的字段。在本例中,我们只使用一个title
andcontent
字段。
因此,让我们在项目中添加一个名为的新文件夹,并在其中models
创建一个名为的文件。Post.js
const mongoose = require("mongoose")
const schema = mongoose.Schema({
title: String,
content: String
})
module.exports = mongoose.model("Post", schema)
在这里,我们使用 构建一个模式mongoose.Schema
,并定义字段和数据类型。然后,我们基于mongoose.model
刚刚创建的模式,使用 创建一个新模型。
获取所有帖子
在这里,我们可以创建一个名为的新文件routes.js
,其中将包含我们的 Express 路线。
const express = require("express")
const router = express.Router()
module.exports = router
我们还需要导入express
,但这次我们想使用express.Router
。它允许我们注册路由并在我们的应用程序中使用它(在index.js
)。
现在,我们准备在 Express 中创建第一条真正可以执行某些操作的路线!
让我们创建一个可以获取现有帖子列表的路线。
const express = require("express")
const Post = require("./models/Post") // new
const router = express.Router()
// Get all posts
router.get("/posts", async (req, res) => {
const posts = await Post.find()
res.send(posts)
})
module.exports = router
在这里,我们导入Post
模型并创建一个GET
带有方法的新路由router.get
。此方法将接受路由的端点,以及路由处理程序来定义应发送到客户端的数据。在本例中,我们将从find
模型中获取所有帖子,并使用方法发送结果res.send
。
由于从数据库获取文档是异步的,我们需要使用await
来等待操作完成。所以,别忘了将你的函数标记为async
。这样,在数据完全获取后,我们就可以将其发送到客户端。
现在,我们可以在我们的中安装我们的路线index.js
。
const express = require("express")
const mongoose = require("mongoose")
const routes = require("./routes") // new
mongoose
.connect("mongodb://localhost:27017/acmedb", { useNewUrlParser: true })
.then(() => {
const app = express()
app.use("/api", routes) // new
app.listen(5000, () => {
console.log("Server has started!")
})
})
首先,我们导入./routes.js
文件来获取所有的路由,并用app.use
为前缀的方法注册它/api
,这样,我们所有的帖子都可以在 中访问/api/posts
。
尝试运行我们的服务器并获取/api/posts
,让我们看看我们得到了什么。
$ curl http://localhost:5000/api/posts
[]
现在,我们从服务器获取了一个空数组。这是因为我们还没有创建任何帖子。那么,为什么不创建一个呢?
创建帖子
要创建帖子,我们需要接受POST
来自的请求/api/posts
。
// ...
router.post("/posts", async (req, res) => {
const post = new Post({
title: req.body.title,
content: req.body.content
})
await post.save()
res.send(post)
})
这里,我们创建一个新Post
对象,并从属性中填充字段req.body
。该req
对象包含客户端请求数据,而请求体就是其中之一。
然后,我们还需要使用该方法保存记录save
。保存数据也是异步的,因此我们需要使用 async/await 语法。
默认情况下,Express 不知道如何读取请求体。这就是为什么我们需要使用body-parser
将请求体解析为 JavaScript 对象。
const express = require("express")
const mongoose = require("mongoose")
const routes = require("./routes")
const bodyParser = require("body-parser") // new
mongoose
.connect("mongodb://localhost:27017/acmedb", { useNewUrlParser: true })
.then(() => {
const app = express()
app.use("/api", routes)
app.use(bodyParser.json()) // new
app.listen(5000, () => {
console.log("Server has started!")
})
})
在这里,我们使用body-parser
库作为中间件来解析 JSON 主体,以便我们可以通过req.body
路由处理程序访问它。
让我们测试一下我们刚刚创建的创建帖子功能!
$ curl http://localhost:5000/api/posts \
-X POST \
-H "Content-Type: application/json" \
-d '{"title":"Post 1", "content":"Lorem ipsum"}'
{
"_v": 0,
"_id": <OBJECT_ID>,
"title": "Post 1",
"content": "Lorem ipsum"
}
您也可以使用Postman进行测试。
获取个人帖子
为了抓取单个帖子,我们需要创建一个新的路线和GET
方法。
// ...
router.get("/posts/:id", async (req, res) => {
const post = await Post.findOne({ _id: req.params.id })
res.send(post)
})
这里,我们注册了一个以 为端点的新路由/posts/:id
。这被称为 URL 参数,它允许我们id
在路由处理程序中获取帖子的 。因为我们存储在数据库中的每个文档都有一个名为 的唯一标识符ObjectID
。我们可以使用此方法找到它,并从对象findOne
中传递 id 。req.params
太棒了,现在尝试使用我们的 HTTP 客户端获取单个博客文章。
$ curl http://localhost:5000/api/posts/<OBJECT_ID>
{
"_id": <OBJECT_ID>,
"title": "Post 1",
"content": "Lorem ipsum"
}
看起来它正在工作,但是还有一件事。
如果我们在这个路由中传递了错误的 ObjectID,我们的服务器就会崩溃。服务器崩溃的原因是,当我们获取一个 ObjectID 不存在的帖子时,Promise 会被拒绝,我们的应用程序就会停止工作。
为了防止这种情况,我们可以用 try/catch 块包装我们的代码,这样每当客户端请求不存在的数据时,我们就可以发送自定义错误。
// ...
router.get("/posts/:id", async (req, res) => {
try {
const post = await Post.findOne({ _id: req.params.id })
res.send(post)
} catch {
res.status(404)
res.send({ error: "Post doesn't exist!" })
}
})
现在,如果我们尝试获取不存在的帖子,我们的服务器仍然会正常运行。
$ curl http://localhost:5000/api/posts/<OBJECT_ID>
{
"error": "Post doesn't exist!"
}
更新帖子
通常,对单个记录执行更新操作的首选 HTTP 方法是PATCH
。那么,让我们创建一个吧!
// ...
router.patch("/posts/:id", async (req, res) => {
try {
const post = await Post.findOne({ _id: req.params.id })
if (req.body.title) {
post.title = req.body.title
}
if (req.body.content) {
post.content = req.body.content
}
await post.save()
res.send(post)
} catch {
res.status(404)
res.send({ error: "Post doesn't exist!" })
}
})
我们的更新帖子路由与获取单个帖子路由类似。我们根据 ID 查找帖子,如果帖子不存在则抛出自定义错误。但这次,我们还会更新帖子对象的每个字段,并使用客户端在 .js 内部提供的数据进行填充req.body
。
我们还想用save
方法保存我们的帖子对象,并将更新的帖子数据发送给客户端。
现在,我们可以运行一个PATCH
方法到我们的/api/posts/<OBJECT_ID>
端点。
$ curl http://localhost:5000/api/posts/<OBJECT_ID> \
-X PATCH \
-H "Content-Type: application/json" \
-d '{"title":"Updated Post", "content":"Updated post content"}'
{
"__v": 0,
"_id": <OBJECT_ID>,
"title": "Updated Post"
"content": "Updated Post content",
}
删除帖子
最后,我们的最后一步是通过添加删除功能来完成 CRUD 功能。
// ...
router.delete("/posts/:id", async (req, res) => {
try {
await Post.deleteOne({ _id: req.params.id })
res.status(204).send()
} catch {
res.status(404)
res.send({ error: "Post doesn't exist!" })
}
})
在删除帖子路由中,我们基本上只是直接使用方法向数据库执行删除操作deleteOne
,并传递文档 ID。我们不会向用户返回任何内容。
$ curl http://localhost:5000/posts/<OBJECT_ID> -X DELETE -I
HTTP/1.0 204 NO CONTENT
...