使用 Node.js/MongoDB/Passport/JWT 构建 REST API
使用 Node.js / MongoDB / Passport / JWT 构建 REST API
如果你之前不了解 Node 和 JS,可以参加Wes Bos 的这门课程,它能帮助你快速入门。
一门高级培训课程,教你如何使用 Node.js、Express、MongoDB 等技术构建应用程序。 立即开始学习 →
在本课中,我们将开始使用NodeJS和MongoDB数据库构建 REST API 的旅程。如果您之前没有 NodeJS 和 MongoDB 的经验,那么本课将非常适合您。
为什么要学习这篇教程?
我刚开始学习编程的时候,一直在寻找解决问题的方法,也确实找到了。但问题是,我不明白为什么有些代码有时有效,有时无效。我只能复制别人的源代码,却不明白其中的原理。
本教程将帮助您了解所有可以使用的样板代码,并让您理解其中的每个部分。
我们将制作什么?
我们将创建一个与 Medium 网站非常相似的网站,并采用 REST 标准。我们还将使用以下功能:
- 身份验证本地 + JWT
- 用户可以创建帖子
- 用户可以删除自己的帖子并进行更新。
- 用户可以关注其他用户的帖子。
- 用户收到他关注的用户发布的帖子通知
- 用户可以点赞帖子
- 用户可以看到他喜欢过的所有职位列表。
听起来很有趣,对吧?让我们来看看我们将使用哪些工具来制作这款精彩的应用程序。
该应用程序的技术栈
我们将使用 JavaScript、ES6 和 ES7,并使用 Babel 和 Webpack v2 编译源代码。您还应该熟悉 JavaScript 的 Promise 和异步操作。
数据库方面,我们将使用 MongoDB。
设置工具
在本系列的第一部分中,我们将使用以下工具搭建我们的环境:
- 编辑器配置
- 表达
- 埃斯林特
- 巴别塔
- Webpack 2
读完这篇文章,我们就能搭建并运行一个简单的 Express 服务器了。让我们开始吧!
为你的项目创建一个新目录。我把它命名为“ makenodejsrestapi”。我将使用yarn包来安装我的工具。在该目录下,我们首先创建一个名为“.gitignore”的新文件,并添加以下内容:
node_modules/
现在,我们将运行以下命令来初始化我们的项目:
yarn init
系统会询问您各种问题,我直接按回车键,让 yarn 使用默认值。命令执行完毕后,您会在项目目录中看到一个名为 _package.json_ 的新文件,其内容如下:
{
“name”: “makenodejsrestapi”,
“version”: “1.0.0”,
“main”: “index.js”,
“license”: “MIT”
}
此文件仅包含我们项目的元数据。接下来,我们将开始在项目中添加 Express。请运行以下命令:
yarn add express
如果初始状态下找不到该包,yarn需要一些时间才能找到它,但最终肯定会找到。命令运行完毕后,我们的package.json 文件将更新为以下内容:
接下来,我们在项目中创建一个名为 src 的新目录,并在其中创建一个名为 index.js 的新文件。将以下内容放入该文件中:
import express from 'express';
const app = express();
const PORT = process.env.PORT || 3000;
app.listen(PORT, err => {
if (err) {
throw err;
} else {
console.log(Server running on port: $ {
PORT
}-- - Running on $ {
process.env.NODE\_ENV
}-- - Make something great!)
}
});
请注意,如果环境变量中未设置端口,我们将使用端口 3000。现在,我们将在 package.json 文件中添加一个“script”,以便在使用 Babel 运行时使用开发环境。以下是修改后的文件:
现在,使用 yarn 运行以下命令安装 cross-env:
yarn add cross-env
这是更新后的 package.json 文件:
{
"name": "makenodejsrestapi",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"dev": "NODE\_ENV=development node src/index.js"
},
"dependencies": {
"cross-env": "^5.1.3",
"express": "^4.16.2"
}
}
现在我们可以使用以下命令添加 Babel 依赖项:
yarn add -D babel-preset-env babel-plugin-transform-object-rest-spread
运行命令后,您可以创建一个名为 .babelrc 的文件,并在其中提供有关应用程序的环境和插件信息。接下来我们将这样做:
{
"presets": [
["env", {
"targets": {
"node": "current"
}
}]
],
"plugins": [
["transform-object-rest-spread", {
"useBuiltIns": true
}]
]
}
transform-object-rest-spread 插件用于转换对象解构赋值的剩余属性。现在,我们也将使用 webpack 2:
yarn add -D webpack babel-core babel-loader webpack-node-externals
最后,我们将配置 webpack,因为我们也在上面添加了它的依赖项:
const nodeExternals = require('webpack-node-externals');
const path = require('path');
module.exports = {
target: 'node',
externals: [nodeExternals()],
entry: {
'index': './src/index.js'
},
output: {
path: path.join(\_\_dirname, 'dist'),
filename: '[name].bundle.js',
libraryTarget: 'commonjs2',
},
module: {
rules: [{
test: /\.js$/,
exclude: /node\_modules/,
use: 'babel-loader'
}]
}
}
现在,我们也运行 package.json 脚本:
"scripts": { "dev:build": "webpack -w", "dev": "cross-env NODE\_ENV=development node dist/index.bundle.js" }
最后,我们可以运行我们的应用程序了:
为了更直观地查看,以下是运行构建后的输出结果:
请注意,上面我们运行了两条命令:
- 第一条命令只是构建了应用程序并准备了 Babel 构建。
- 第二条命令实际执行请求,您可以在控制台中看到输出结果。
现在,我们终于要安装 ES Lint 了:
yarn add -D eslint eslint-config-equimper
现在,创建一个名为“.eslintrc”的新文件,并添加以下内容:
{ “extends” : “equimper” }
完成此操作后,如果您未遵循正确的 ES 标准,将会收到警告。当您的项目需要严格遵循规范时,此工具非常有用。
一门高级培训课程,教你如何使用 Node.js、Express、MongoDB 等技术构建应用程序。立即开始学习 →
接下来我们将添加什么?
在本节中,我们将设置此应用程序后端所需的更多工具:
- 添加猫鼬、身体解析器、摩根、压缩、头盔
- 设置配置文件夹
- 设置常量
添加 Mongoose
要将 mongoose 和其他提到的模块添加到您的应用程序中,请运行以下命令:
yarn add mongoose body-parser compression helmet && yarn add -D morgan
需要注意的是,我们指定的模块顺序,将按照相同的顺序下载。
为了确保我们理解一致,以下是我的 package.json 文件内容:
现在,我们将使用以下命令再次编译我们的项目:
yarn dev
请确保项目仍在运行。现在,在 src 文件夹内新建一个 config 文件夹,并在其中创建一个名为 constants.js 的文件,内容如下:
const devConfig = {};
const testConfig = {};
const prodConfig = {};
const defaultConfig = {
PORT: process.env.PORT || 3000,
};
function envConfig(env) {
switch (env) {
case 'development':
return devConfig;
case 'test':
return testConfig;
default:
return prodConfig;
}
}
//Take defaultConfig and make it a single object
//So, we have concatenated two objects into one
export default { ...defaultConfig,
...envConfig(process.env.NODE\_ENV),
};
现在,回到 index.js 文件,我们将添加对该常量文件的依赖,并将对 PORT 的引用更改为使用此文件,如下所示:
import express from 'express';
import constants from './config/constants';
const app = express();
app.listen(constants.PORT, err => {
if (err) {
throw err;
} else {
console.log(`Server running on port: ${constants.PORT} --- Running on ${process.env.NODE_ENV} --- Make something great.!`)
}
});
现在,在 config 文件夹中创建一个名为 database.js 的新文件,并添加以下内容:
import mongoose from 'mongoose';
import constants from './constants';
//Removes the warning with promises
mongoose.Promise = global.Promise;
//Connect the db with the url provided
try {
mongoose.connect(constants.MONGO\_URL)
} catch (err) {
mongoose.createConnection(constants.MONGO\_URL)
}
mongoose.connection.once('open', () => console.log('MongoDB Running')).on('error', e => {
throw e;
})
我们还修改了 constants.js 文件中的 mongoose 连接配置,如下所示:
const devConfig = { MONGO\_URL: 'mongodb://localhost/makeanodejsapi-dev', };
const testConfig = { MONGO\_URL: 'mongodb://localhost/makeanodejsapi-test', };
const prodConfig = { MONGO\_URL: 'mongodb://localhost/makeanodejsapi-prod', };
这样可以确保我们在使用不同配置文件和环境运行应用程序时,使用的数据库是不同的。您可以继续再次运行此应用程序。
当数据库在该端口上运行后,即可成功开始使用您的应用程序。
中间件设计
现在,我们将开始制作应用程序的中间件。
在 config 文件夹中,新建一个名为middleware.js 的文件,并添加以下内容:
import morgan from 'morgan';
import bodyParser from 'body-parser';
import compression from 'compression';
import helmet from 'helmet';
import {
isPrimitive
} from 'util';
const isDev = process.env.NODE\_ENV === 'development';
const isProd = process.env.NODE\_ENV === 'production';
export default app => {
if (isProd) {
app.use(compression());
app.use(helmet());
}
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
extended: true
}));
if (isDev) {
app.use(morgan('dev'));
}
};
要使用此配置,还需要将导入语句添加到索引文件中,例如:
import express from 'express';
import constants from './config/constants';
import './config/database';
import middlewareConfig from './config/middleware';
const app = express(); //passing the app instance to middlewareConfig
middlewareConfig(app);
app.listen(constants.PORT, err => {
if (err) {
throw err;
} else {
console.log(`Server running on port: ${constants.PORT} --- Running on ${process.env.NODE_ENV} --- Make something great.!`)
}
});
现在,运行你的应用程序,它应该会在 3000 端口上处理一个 GET 请求!
注册用户
在本节中,我们将使用上一课中搭建的 MongoDB 环境,并以此为基础构建一个允许用户注册的应用程序。为了方便您理解,我们最新的 package.json 文件内容如下:
在本节中,我们将继续推进,实现用户注册功能。我们还将创建用户模型,以便将数据保存到数据库中。
完成本节后,您的文件结构至少应如下所示:
跟着课程一步步来,看看结果如何!
定义模型
我们将首先创建 User 模型。为此,请在 src > modules > users 目录下创建一个名为 user.model.js 的新文件,并添加以下内容:
import mongoose, {
Schema
} from 'mongoose';
import validator from 'validator';
import {
passwordReg
} from './user.validations';
const UserSchema = new Schema({
email: {
type: String,
unique: true,
required: [true, 'Email is required!'],
trim: true,
validate: {
validator(email) {
return validator.isEmail(email);
},
message: '{VALUE} is not a valid email!',
},
},
firstName: {
type: String,
required: [true, 'FirstName is required!'],
trim: true,
},
lastName: {
type: String,
required: [true, 'LastName is required!'],
trim: true,
},
userName: {
type: String,
required: [true, 'UserName is required!'],
trim: true,
unique: true,
},
password: {
type: String,
required: [true, 'Password is required!'],
trim: true,
minlength: [6, 'Password need to be longer!'],
validate: {
validator(password) {
return passwordReg.test(password);
},
message: '{VALUE} is not a valid password!',
},
},
});
export default mongoose.model('User', UserSchema);
我们刚刚定义了用户模型的架构,其中包含各种属性,例如:
- 为用户定义的属性
- 此外,还提供了关于其类型属性、唯一性以及如何验证这些数据的元信息。
- 请注意,我们还提供了一个验证函数。这使得向 MongoDB 集合中插入数据变得非常简单。
定义控制器
现在,我们将通过在 Controller 定义中使用 User 模型来启用它。在 src > modules > users 目录下创建一个名为 user.controllers.js 的新文件,并添加以下内容:
import User from './user.model';
export async function signUp(req, res) {
try {
const user = await User.create(req.body);
return res.status(201).json(user);
} catch (e) {
return res.status(500).json(e);
}
}
我们刚刚定义了一个 signUp 函数,它以请求对象和响应对象作为参数,并使用我们上面定义的 User 模型创建了它。
我们还返回了相应的响应以及他们的代码,以便用户在交易成功时收到通知。
定义应用程序路由
我们将为应用程序定义路由,以便指定用户必须访问哪些路径才能访问我们开发的应用程序。在 src > modules > users 目录下创建一个名为 user.routes.js 的新文件,并添加以下内容:
import {
Router
} from 'express';
import \* as userController from './user.controllers';
const routes = new Router();
routes.post('/signup', userController.signUp);
export default routes;
请注意,这样做行不通。我们必须在 modules 文件夹内定义模块 index.js,并添加以下内容:
import userRoutes from './users/user.routes';
export default app => {
app.use('/api/v1/users', userRoutes);
};
现在我们可以运行我们的应用程序了,这将是我们应用程序的第一个实际版本。现在我们只需要对根目录下的 index.js 文件进行最后的修改:
以下是更新后的内容:
/\* eslint-disable no-console \*/
import express from 'express';
import constants from './config/constants';
import './config/database';
import middlewaresConfig from './config/middlewares';
import apiRoutes from './modules';
const app = express();
middlewaresConfig(app);
app.get('/', (req, res) => {
res.send('Hello world!');
});
apiRoutes(app);
app.listen(constants.PORT, err => {
if (err) {
throw err;
} else {
console.log(` Server running on port: ${constants.PORT} --- Running on ${process.env.NODE_ENV} --- Make something great `);
}
});
现在运行该应用时,我们仍然可以看到应用正在运行:
Postman 非常适合 API 测试,学习这门课程可以帮助你了解它的用途。
一门高级培训课程,教你如何使用 Node.js、Express、MongoDB 等技术构建应用程序。立即开始学习 →
使用 MongoDB 和 Postman
接下来我们将使用两种必要的工具:
- Robomongo:点击此处下载。它是一款非常棒的工具,可以用来可视化 MongoDB 数据并进行查询。而且它是免费的!它适用于所有操作系统平台。
- Postman:点击此处下载。它是一款用于调用 API 并获取响应的工具。它拥有强大的可视化功能,还可以保存请求格式,从而节省大量时间。再次强调,它是免费的!Postman 支持所有操作系统平台。
打开 Robomongo 并连接到本地 MongoDB 实例后,您可以看到数据库已经存在:
我们已经准备好了一个由我们的应用程序生成的收藏集,它看起来会像这样:
目前这里是空的,因为我们还没有创建任何数据。我们很快就会创建!
尝试用户注册
现在我们打开 Postman。我们将使用以下 URL 调用 API:
http://localhost:3000/api/v1/users/signup
在 Postman 中,它看起来会像这样:
在调用此 API 之前,我们将尝试一个 Hello World 版本。看看此 API 会发生什么:
现在,我们回到注册 API。在成功调用之前,我们将尝试提供无效值,看看会遇到什么错误。如果邮箱地址无效,结果如下:
现在,我们再用正确的数据试试。开始吧!
故事还没结束。我们现在还可以看到数据正在被插入到 MongoDB 数据库中:
出色的!
添加更多验证
虽然我们已经在 User 模型中添加了一些验证,但如果我们也想在另一个文件中保留这些验证呢?为此,请在 src > modules > users 目录下创建一个名为 user.validations.js 的新文件,并添加以下内容:
import Joi from 'joi';
export const passwordReg = /(?=.\*\d)(?=.\*[a-z])(?=.\*[A-Z]).{6,}/;
export default {
signup: {
email: Joi.string().email().required(),
password: Joi.string().regex(passwordReg).required(),
firstName: Joi.string().required(),
lastName: Joi.string().required(),
userName: Joi.string().required(),
},
};
接下来,在路由文件中也添加此验证:
import {
Router
} from 'express';
import validate from 'express-validation';
import \* as userController from './user.controllers';
import userValidation from './user.validations';
const routes = new Router();
routes.post('/signup', validate(userValidation.signup), userController.signUp);
export default routes;
请注意:
- 我们添加了从 Express 导入验证的功能。
- 我们还添加了一个新的用户注册验证函数。
现在当我们尝试使用相同的凭据注册用户时,会收到错误提示:
这其实不是验证的问题,而是因为我们试图插入另一个使用相同邮箱的用户。我们换个方法试试:
现在,我们可以修正这个问题,并在 MongoDB 数据库中看到数据:
太棒了!我们成功地为项目添加了强大的验证功能。
密码加密和用户登录
我们将更多地与用户互动。在上一节课中,我们成功保存了一个新用户。但这种方法的主要问题是,用户的密码以明文形式保存。这将是我们现在要在应用程序中做出的改进之一。
为了确保您不会错过任何重要信息,我们最新的 package.json 文件内容如下:
在本课中,我们将继续推进,实现用户密码加密功能。除此之外,我们还将进行以下更改:
- 在 webpack 构建中添加rimraf和 clean dist
- 对用户密码进行加密
- 利用护照制定本地战略
- 允许用户登录
添加 rimraf 依赖项
我们将首先使用以下命令在项目中添加rimraf依赖项:
yarn add -D rimraf
要重新构建您的项目,请运行以下命令:
yarn
现在,让我们把 rimraf 也添加到 package.json 文件中:
“scripts”: {
“clean”: “rimraf dist”,
“dev:build”: “yarn run clean && webpack -w”,
“dev”: “cross-env NODE\_ENV=development nodemon dist/index.bundle.js”
}
现在,运行以下命令:
yarn dev:build
运行此命令后,dist 文件夹将被刷新,并在构建过程完成后恢复:
用于加密密码的库
现在,我们将向项目中添加一个库,以便在将用户密码保存到数据库之前对其进行加密。这样,即使数据库遭到黑客攻击,我们也能确保密码安全。
运行以下命令:
yarn add bcrypt-nodejs
这样,该库就会添加到我们的项目中。
修改模型
现在,我们需要修改模型,以便在收到明文密码请求时,能够设置一个加密后的密码。请在 user.model.js 文件中添加以下代码。
UserSchema.pre('save', function(next) {
if (this.isModified('password')) {
this.password = this.\_hashPassword(this.password);
}
return next();
});
UserSchema.methods = {
\_hashPassword(password) {
return hashSync(password);
},
authenticateUser(password) {
return compareSync(password, this.password);
},
};
在上述代码中,`this` 指的是请求中指定的当前用户。此外,`authenticateUser` 函数会在我们尝试登录时立即调用,用户会传递一个明文密码。我们会对该密码进行哈希处理,之后才会将其与数据库中的值进行比较。
现在,我们尝试发送一个新的请求,看看是否有效。以下是我的请求:
当我运行此请求时,我们得到的响应如下:
现在我们来查看一下数据库,也会看到类似的情况:
现在,我们将为我们的应用程序提供一个登录 API。
使用 Passport 登录
我们将使用名为Passport 的库。您也可以自由使用任何其他身份验证库,例如 Facebook、Google 等。
接下来,我们需要向项目中添加两个库。让我们运行以下命令来完成此操作:
yarn 添加 passport passport-local
完成上述步骤后,我们在src文件夹内创建一个名为 services 的新文件夹。我们将在 services 文件夹内创建一个名为 auth.services.js 的新文件,并添加以下内容:
import passport from 'passport';
import LocalStrategy from 'passport-local';
import User from '../modules/users/user.model';
const localOpts = {
usernameField: 'email',
};
const localStrategy = new LocalStrategy(localOpts, async (email, password, done) => {
try {
const user = await User.findOne({
email
});
if (!user) {
return done(null, false);
} else if (!user.authenticateUser(password)) {
return done(null, false);
}
return done(null, user);
} catch (e) {
return done(e, false);
}
});
passport.use(localStrategy);
export const authLocal = passport.authenticate('local', {
session: false
});
在这里,我们尝试了一种本地策略,该策略本质上是异步的,数据(用户的电子邮件地址和密码)会发送到 Passport 库。然后,该库将验证用户身份并返回响应。
我们还会添加 Passport 作为中间件。以下是修改后的文件:
import morgan from 'morgan';
import bodyParser from 'body-parser';
import compression from 'compression';
import helmet from 'helmet';
import passport from 'passport';
const isDev = process.env.NODE\_ENV === 'development';
const isProd = process.env.NODE\_ENV === 'production';
export default app => {
if (isProd) {
app.use(compression());
app.use(helmet());
}
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
extended: true
}));
app.use(passport.initialize());
if (isDev) {
app.use(morgan('dev'));
}
};
在这里,我们也使用我们的应用程序实例初始化了 Passport 库。
向控制器添加登录信息
现在是时候在控制器层添加登录功能了。请将以下函数添加到控制器中:
export function login(req, res, next) {
res.status(200).json(req.user);
return next();
}
请注意,这是我们最终的 Controller 文件的样子:
提供登录路线
我们还需要提供登录 API 的路由。我们将修改 user.routes.js 文件。请将以下路由及其导入语句添加到该文件中:
import {
authLocal
} from ‘../../services/auth.services’;
routes.post(‘/login’, authLocal, userController.login);
以下是我们最终文件的样子:
尝试登录功能
现在我们将使用之前创建的凭据尝试以下 POST API:
http://localhost:3000/api/v1/users/login
如果凭证正确,则会发生以下情况:
这真是太棒了!我们不仅成功登录了现有用户,还通过加密保护了他的密码。
添加 JWT 身份验证
目前,我们能够在应用程序中注册新用户:
我们还允许用户登录我们的应用程序:
在了解本文将要制作的内容之前,让我们先来看看当前的 _package.json_file 文件是什么样子:
在本节中,我们将添加以下功能:
- 我们将实现 JWT 身份验证并添加一个密钥密码
- 添加新的 passport-jwt 库
- 添加 JSON Web Token 库
- 仅以 JSON 格式发送必需字段作为响应。
JSON Web Token 如何存储数据?
当我们提供要加密的数据以及秘密密码时,这些数据将被加密以形成 JWT 令牌的各个部分,例如:
如上所示,一个令牌可以包含用户的身份信息以及与该用户相关的其他数据。
添加 JWT 密钥
接下来,我们打开 _constants.js_file 文件,并在开发配置中添加一个 JWT 密钥:
const devConfig = {
MONGO\_URL: ‘mongodb://localhost/makeanodejsapi-dev’,
JWT\_SECRET: ‘thisisasecret’,
};
接下来,我们将使用以下命令安装两个库:
yarn add jsonwebtoken passport-jwt
现在,转到身份验证服务文件和 JWT 服务文件,并在该文件中添加以下代码行:
import { Strategy as JWTStrategy, ExtractJwt } from ‘passport-jwt’;
import User from ‘../modules/users/user.model’;
import constants from ‘../config/constants’;
接下来,让 Passport 使用指定的策略:
// Jwt strategy
const jwtOpts = {
jwtFromRequest: ExtractJwt.fromAuthHeader('authorization'),
secretOrKey: constants.JWT\_SECRET,
};
const jwtStrategy = new JWTStrategy(jwtOpts, async (payload, done) => {
try {
//Identify user by ID
const user = await User.findById(payload.\_id);
if (!user) {
return done(null, false);
}
return done(null, user);
} catch (e) {
return done(e, false);
}
});
passport.use(localStrategy);
passport.use(jwtStrategy);
export const authLocal = passport.authenticate('local', { session: false });
export const authJwt = passport.authenticate('jwt', { session: false });
为了测试这种方法是否有效,我们现在将在路由JS文件中使用私有路由。最终的文件内容如下所示:
import userRoutes from ‘./users/user.routes’;
import { authJwt } from ‘../services/auth.services’;
export default app => {
app.use(‘/api/v1/users’, userRoutes);
app.get(‘/hello’, authJwt, (req, res) => {
res.send(‘This is a private route!!!!’);
});
};
验证 JWT
我们来试试看,验证一下 JWT 现在是否能在 Postman 中正常工作:
现在我们需要在请求中添加一个只属于特定用户的 JWT 令牌。
我们将为 User 模型添加功能,使其在用户登录时也包含 JWT 令牌。因此,让我们向 User 模型 JS 文件添加更多库:
import jwt from ‘jsonwebtoken’;
import constants from ‘../../config/constants’;
现在我们可以解密令牌并获取用户信息。
创建 JWT 令牌
我们还需要创建一个为用户生成令牌的方法。现在就来添加这个方法:
UserSchema.methods = {
createToken() {
return jwt.sign(
{
\_id: this.\_id,
},
constants.JWT\_SECRET,
);
},
toJSON() {
return {
\_id: this.\_id,
userName: this.userName,
token: `JWT ${this.createToken()}`,
};
},
};
使用 toJSON() 方法也很重要。我们在令牌前面附加了 JWT,因为 Passport 库使用它来识别 JWT 令牌。
现在,我们再尝试登录用户:
这次我们甚至还收到了 JWT 令牌作为响应。该令牌将包含用户 ID 和用户名。现在我们有了一个可用的 JWT 示例!
让我们复制 JWT 值,现在尝试使用私有路由:
通过用户和对象关联发布帖子
接下来,我们就可以在应用程序中注册新用户了:
我们还允许用户登录我们的应用程序:
在了解本文将要制作的内容之前,让我们先来看看当前的package.json文件是什么样子:
在本节中,我们将添加以下功能:
- 我们将为帖子创建一个新的资源。现在,用户也可以创建帖子了。
- 将用户设为帖子作者
- 解决我们在之前的帖子中提出的一些问题
创建帖子模型
就像我们之前对 User 模型所做的那样,Post 模型也需要进行同样的操作,例如创建一个新文件夹。在本课程结束时,您的项目中将包含以下新文件夹和文件:
我们将首先创建 Post 模型。我们还会添加所需的验证。接下来,我们再添加一个用于 Mongoose 唯一性验证的库:
yarn add mongoose-unique-validator
我们还将添加一个新的 Slug 库。为此,请使用以下命令安装它:
纱线添加蛞蝓
如果你想知道什么是 slugify,简单来说,就是文章的 URL 应该看起来像文章标题。这样看起来美观,而且文章内容也能在 URL 中一览无余,这是一个很好的做法。
现在,我们也可以添加这个库了。我们的模型将如下所示:
import mongoose, { Schema } from 'mongoose';
import slug from 'slug';
import uniqueValidator from 'mongoose-unique-validator';
const PostSchema = new Schema({
title: {
type: String,
trim: true,
required: [true, 'Title is required!'],
minlength: [3, 'Title need to be longer!'],
unique: true,
},
text: {
type: String,
trim: true,
required: [true, 'Text is required!'],
minlength: [10, 'Text need to be longer!'],
},
slug: {
type: String,
trim: true,
lowercase: true,
},
user: {
type: Schema.Types.ObjectId,
ref: 'User',
},
favoriteCount: {
type: Number,
default: 0,
},
}, { timestamps: true });
PostSchema.plugin(uniqueValidator, {
message: '{VALUE} already taken!',
});
PostSchema.pre('validate', function (next) {
this.\_slugify();
next();
});
PostSchema.methods = {
\_slugify() {
this.slug = slug(this.title);
},
};
PostSchema.statics = {
createPost(args, user) {
return this.create({
...args,
user,
});
},
};
export default mongoose.model('Post', PostSchema);
我们在上述模型中进行了以下操作:
- Post 模型定义的字段
- 针对每个字段添加了验证
- 为整个 Post 对象添加了验证
- 我们根据帖子标题对其进行 slug 化处理,并将该值保存下来。
在上面的代码中,接下来我们将在控制器中添加 createPost 方法。
创建帖子控制器
现在我们需要一个控制器,以便用户能够实际执行与帖子相关的操作。
根据上面所示的目录结构,在 post 模块中定义一个新的文件 post.controller.js,并添加以下内容:
import Post from './post.model';
export async function createPost(req, res) {
try {
const post = await Post.createPost(req.body, req.user.\_id);
return res.status(201).json(post);
} catch (e) {
return res.status(400).json(e);
}
}
当遇到错误或成功创建新帖子时,我们会返回相应的响应。
创建帖子路由
现在让我们在应用程序中的 posts 模块下的 post.route.js 文件中创建指向 Post 控制器的路由,内容如下:
import { Router } from 'express';
import \* as postController from './post.controllers';
import { authJwt } from '../../services/auth.services';
const routes = new Router();
routes.post(
'/',
authJwt,
);
export default routes;
我们也要修改 index.js 文件来实现这个功能。最终内容如下:
import userRoutes from ‘./users/user.routes’;
import postRoutes from ‘./posts/post.routes’;
export default app => {
app.use(‘/api/v1/users’, userRoutes);
app.use(‘/api/v1/posts’, postRoutes);
};
验证帖子 API
现在我们将尝试使用 POST API 创建一个新帖子。
首先,尝试登录用户,以便获取 JWT 令牌,然后通过此 URL 调用创建帖子 API:
http://localhost:3000/api/v1/posts
以下是我们尝试的方法以及得到的反馈:
我们已经填充了日期和别名字段。其中也包含用户 ID。让我们也看看 MongoDB 中的这篇文章:
如果我们再次调用此 API 创建帖子,将会失败,因为标题已被占用:
这意味着我们的验证也运行正常。
将标题设为强制性
我们可以实施更多验证措施,例如将帖子标题设为必填项。
让我们在 posts 模块中创建一个名为 post.validations.js 的新文件,并添加以下内容:
import Joi from 'joi';
export const passwordReg = /(?=.\*\d)(?=.\*[a-z])(?=.\*[A-Z]).{6,}/;
export default {
signup: {
body: {
email: Joi.string().email().required(),
password: Joi.string().regex(passwordReg).required(),
firstName: Joi.string().required(),
lastName: Joi.string().required(),
userName: Joi.string().required(),
},
},
};
我们还需要修改路由文件,以包含此验证。以下是修改后的文件:
import { Router } from 'express';
import validate from 'express-validation';
import \* as postController from './post.controllers';
import { authJwt } from '../../services/auth.services';
import postValidation from './post.validations';
const routes = new Router();
routes.post(
'/',
authJwt,
validate(postValidation.createPost),
postController.createPost,
);
export default routes;
我们已从上面使用的 authJwtobject 中获取到用户 ID。现在我们收到的消息是:
我们将尽快修改回复方式,使其更加得体。
通过 ID 获取数据并填充另一个对象。
接下来,我们就可以在应用程序中注册新用户了:
我们还允许用户登录我们的应用程序:
我们还创建了一篇与用户相关的帖子:
在本节中,我们将添加以下功能:
- 我们将通过其 ID 获取帖子。
- 我们还将创建控制器和路由
- 我们将向您展示如何在帖子中填充用户信息。
- 我们将使用的其他库
一门高级培训课程,教你如何使用 Node.js、Express、MongoDB 等技术构建应用程序。立即开始学习 →
将 HTTP 状态库添加到控制器
要添加此库,请运行以下命令:
yarn add http-status
现在,我们也可以在用户控制器中使用这个库了。首先,让我们导入这个库:
import HTTPStatus from 'http-status';
接下来,我们将不再使用控制器中原有的状态码(例如 200 等),而是修改此库提供的状态码,如下所示:
export async function signUp(req, res) {
try {
const user = await User.create(req.body);
return res.status(HTTPStatus.CREATED).json(user.toAuthJSON());
} catch (e) {
return res.status(HTTPStatus.BAD\_REQUEST).json(e);
}
}
export function login(req, res, next) {
res.status(HTTPStatus.OK).json(req.user.toAuthJSON());
return next();
}
我们将在帖子控制器中执行相同的操作:
import HTTPStatus from 'http-status';
import Post from './post.model';
export async function createPost(req, res) {
try {
const post = await Post.createPost(req.body, req.user.\_id);
return res.status(HTTPStatus.CREATED).json(post);
} catch (e) {
return res.status(HTTPStatus.BAD\_REQUEST).json(e);
}
}
按 ID 获取帖子
我们将在帖子控制器中定义一个新函数,用于通过 ID 获取帖子:
export async function getPostById(req, res) {
try {
const post = await Post.findById(req.params.id);
return res.status(HTTPStatus.OK).json(post);
} catch (e) {
return res.status(HTTPStatus.BAD\_REQUEST).json(e);
}
}
接下来,我们来定义这个函数的路由:
routes.get('/:id', postController.getPostById);
我们的 MongoDB 数据库中有以下帖子:
我们将通过我们的 API 获取此帖子:
这个响应的问题在于,我们得到了 MongoDB 中存在的所有字段。这不是我们想要的。让我们在 Post 模型中进行修改:
PostSchema.methods = {
\_slugify() {
this.slug = slug(this.title);
},
toJSON() {
return {
\_id: this.\_id,
title: this.title,
text: this.text,
createdAt: this.createdAt,
slug: this.slug,
user: this.user,
favoriteCount: this.favoriteCount,
};
},
};
在模型中应用 toJSON() 函数后,我们现在会得到如下响应:
在 POST 响应中获取用户数据
仔细观察上面的JSON数据,我们会发现其中包含用户字段,该字段保存着用户的ID。但如果我们还想在同一个对象中保留用户的其他信息呢?
只需稍微修改 getPostById 函数,并将函数中的 post 常量修改如下:
const post = await Post.findById(req.params.id).populate('user');
我们刚刚添加了一个填充调用,现在的响应将是:
当我们填充用户对象时,toJSON 函数也能正常工作。但这里存在一个问题,因为我们还得到了上面提到的 token 字段,这不应该发生!
让我们修改用户模型来改进这一点:
UserSchema.methods = {
\_hashPassword(password) {
...
},
authenticateUser(password) {
...
},
createToken() {
...
},
toAuthJSON() {
...
},
toJSON() {
return {
\_id: this.\_id,
userName: this.userName,
};
},
我们修改了上面的 toJSON 方法,使 token 字段不包含在响应本身中。
问题依然存在。我们来看看尝试登录用户时会发生什么:
你看,这里也没有 token 字段。要解决这个问题,请转到用户控制器中的登录函数,并按如下方式修改:
export function login(req, res, next) {
res.status(HTTPStatus.OK).json(req.user.toAuthJSON());
return next();
}
现在,我已经使用了 toAuthJSON 函数本身。如果您现在尝试登录,应该可以像以前一样正常登录了!
从数据库中获取所有数据
接下来,我们就可以在应用程序中注册新用户了:
我们还允许用户登录我们的应用程序:
我们还创建了一篇与用户相关的帖子:
在本节中,我们将添加以下功能:
- 完善帖子控制器,添加更多功能
扩展控制器
目前,我们的帖子控制器仅具备以下功能:
- 创建帖子
- 按 ID 获取帖子
现在,我们还将添加更多功能,首先我们将获取所有帖子列表。
获取所有帖子
让我们在帖子控制器中添加一个新方法来获取所有帖子,从而扩展其功能:
export async function getPostsList(req, res) {
try {
const posts = await Post.find().populate('user');
return res.status(HTTPStatus.OK).json(posts);
} catch (e) {
return res.status(HTTPStatus.BAD\_REQUEST).json(e);
}
}
这里,我们返回了帖子。让我们修改路由文件,使用上面添加的这个函数:
routes.get('/', postController.getPostsList);
我们尚未在身份验证中添加此功能,以便即使未经身份验证的用户也能至少发布帖子。现在让我们试试这个 API:
目前数据库中有 11 篇文章,因此上述 API 测试没有问题。但是,如果文章数量超过 5 万篇会怎样呢?在这种情况下,我们将面临严重的性能问题。
分页功能来帮忙了
我们可以根据用户请求返回有限数量的文章。在文章模型中,我们可以提供分页参数,例如:
PostSchema.statics = {
createPost(args, user) {
...
},
list({ skip = 0, limit = 5 } = {}) {
return this.find()
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit)
.populate('user');
},
};
列表函数最初只返回前 5 篇文章。如果跳过 5 篇文章,列表函数会返回 5 篇文章,但会先跳过前 5 篇文章。我们也要修改一下控制器:
export async function getPostsList(req, res) {
const limit = parseInt(req.query.limit, 0);
const skip = parseInt(req.query.skip, 0);
try {
const posts = await Post.list({ limit, skip });
return res.status(HTTPStatus.OK).json(posts);
} catch (e) {
return res.status(HTTPStatus.BAD\_REQUEST).json(e);
}
}
现在,当我们提供这些值时,会得到这样的响应:
更新帖子并添加验证
接下来,我们就可以在应用程序中注册新用户了:
我们还允许用户登录我们的应用程序:
我们还创建了一篇与用户相关的帖子:
在本课中,我们将添加以下功能:
- 我们将更新帖子,并确保更新帖子的用户是帖子的作者。
- 创建验证字段
我们将在接下来的课程中添加更多关于帖子操作的内容。
一门高级培训课程,教你如何使用 Node.js、Express、MongoDB 等技术构建应用程序。立即开始学习 →
扩展控制器
目前,我们的帖子控制器仅具备以下功能:
- 创建一个 pos
- 按 ID 获取帖子
- 获取所有帖子列表
现在,我们还将添加更多功能,首先是允许用户更新帖子。
更新帖子
让我们在帖子控制器中添加一个新方法来更新帖子,从而扩展其功能:
export async function updatePost(req, res) {
try {
const post = await Post.findById(req.params.id);
if (!post.user.equals(req.user.\_id)) {
return res.sendStatus(HTTPStatus.UNAUTHORIZED);
}
Object.keys(req.body).forEach(key => {
post[key] = req.body[key];
});
return res.status(HTTPStatus.OK).json(await post.save());
} catch (e) {
return res.status(HTTPStatus.BAD\_REQUEST).json(e);
}
}
这就是我们上面所做的:
- 通过 JWT 令牌确认用户是否与 Post 对象中的用户相同
- 如果用户不是同一人,则返回 UNAUTHORIZED 响应。
- 如果用户相同,我们会获取请求中传递的每个键,并据此更新帖子。
- 所有更新完成后,我们返回 OK 响应
让我们修改验证文件,使用上面添加的这个函数:
import Joi from 'joi';
export default {
createPost: {
body: {
title: Joi.string().min(3).required(),
text: Joi.string().min(10).required(),
},
},
updatePost: {
body: {
title: Joi.string().min(3),
text: Joi.string().min(10),
},
},
};
我们刚刚在 updatePost 函数中添加了字段长度至少为两个的验证。现在该编写路由文件了:
routes.patch(
'/:id',
authJwt,
validate(postValidation.updatePost),
postController.updatePost,
);
更新帖子
现在工作已经完成,我们将验证上述工作。让我们使用 Postman 发送一个 PATCH 请求,如下所示:
太棒了,成功了!连文章的别名都更新了。请确保我们在 Post 模型中添加了这个方法:
PostSchema.pre(‘validate’, function (next) {
this.\_slugify();
next();
});
接下来,也请尝试对帖子文本进行同样的操作。
授权用户删除帖子
目前,我们能够在应用程序中注册新用户:
我们还允许用户登录我们的应用程序:
我们成功创建了一篇与用户相关的帖子:
在本课中,我们将添加以下功能:
- 我们将允许作者删除帖子。
- 授权功能
- 添加一个名为“漂亮”的工具
扩展控制器
目前,我们的帖子控制器仅具备以下功能:
- 创建帖子
- 按 ID 获取帖子
- 获取所有帖子列表
- 更新帖子
现在,我们还将添加更多功能,首先是允许用户删除帖子。
删除帖子
让我们在帖子控制器中添加一个新方法来删除帖子,从而扩展其功能:
export async function deletePost(req, res) {
try {
const post = await Post.findById(req.params.id);
if (!post.user.equals(req.user.\_id)) {
return res.sendStatus(HTTPStatus.UNAUTHORIZED);
}
await post.remove();
return res.sendStatus(HTTPStatus.OK);
} catch (e) {
return res.status(HTTPStatus.BAD\_REQUEST).json(e);
}
}
这就是我们上面所做的:
- 通过 JWT 令牌确认用户是否与 Post 对象中的用户相同
- 如果用户不是同一人,我们将返回 UNAUTHORIZED 响应。
- 如果用户是同一人,我们将删除该帖子。
- 一旦帖子被删除,我们就返回 OK 响应。
现在该处理路由文件了:
routes.delete('/:id', authJwt, postController.deletePost);
删除帖子
现在工作已经完成,我们将验证上述工作。让我们使用 Postman 发送一个 DELETE 请求,如下所示:
现在,您可以使用类似以下的查询来验证此帖子是否既不在“获取所有帖子”API 中,也不在 MongoDB 中:
添加 Prettier 库
我们可以使用以下 yarn 命令添加 prettier 库:
yarn add -D 更漂亮
完成上述步骤后,以下是我的更新后的 package.json 文件:
{
"name": "makeanodejsrestapi",
...,
"scripts": {
...,
"prettier": "prettier --single-quote --print-width 80 --trailing-comma all --write 'src/\*\*/\*.js'"
},
"dependencies": {
...
},
"devDependencies": {
...,
"prettier": "^1.3.1",
...
}
}
我们仅展示了所做的更改。我们还将使用以下命令添加一个 ES 代码检查库:
yarn add -D eslint-config-prettie
现在,我们将创建一个名为 .eslintrc 的新文件,并添加以下注释:
{
“extends”: [
“equimper”,
“prettier”
]
}
如果您忘记添加分号或缩进,只需运行以下命令,它们就会自动添加:
纱线更漂亮
是不是很神奇?:) 这也显示了哪些文件被修改了:
我们将继续使用此命令和库,因为它确实简化了我们的工作!
一门高级培训课程,教你如何使用 Node.js、Express、MongoDB 等技术构建应用程序。立即开始学习 →
收藏帖子并管理帖子统计信息
接下来,我们就可以在应用程序中注册新用户了:
我们还允许用户登录我们的应用程序:
我们成功创建了一篇与用户相关的帖子:
在本节中,我们将添加以下功能:
- 用户在验证身份后可以收藏帖子,收藏也会增加 favoriteCount 计数器变量的值。
- 修改用户和帖子模型以适应此需求。
- 在 Post 中添加递增/递减静态变量
修改用户模型
我们将添加一个新字段来存储用户收藏的帖子。为此,请编辑 _user.model.js_file 文件,并在密码字段之后添加一个新字段:
favorites: {
posts: [{
type: Schema.Types.ObjectId,
ref: 'Post'
}]
}
我们还会添加一个函数来使用此字段:
UserSchema.methods = {
\_hashPassword(password) {
...
},
authenticateUser(password) {
...
},
createToken() {
...
},
toAuthJSON() {
...
},
toJSON() {
...
},
\_favorites: {
async posts(postId) {
if (this.favorites.posts.indexOf(postId) >= 0) {
this.favorites.posts.remove(postId);
} else {
this.favorites.posts.push(postId);
}
return this.save();
}
}
};
扩展后控制器
让我们在这里也添加一个函数,以使用我们在模型中定义的功能。首先在_post.controller.js_file中使用导入语句:
import User from ‘../users/user.model’;
接下来,我们调用 Usermodel 函数:
export async function favoritePost(req, res) {
try {
const user = await User.findById(req.user.\_id);
await user.\_favorites.posts(req.params.id);
return res.sendStatus(HTTPStatus.OK);
} catch (e) {
return res.status(HTTPStatus.BAD\_REQUEST).json(e);
}
}
最后,让我们修改 _post.routes.js_file 文件以访问此函数:
routes.post(‘/:id/favorite’, authJwt, postController.favoritePost);
现在是时候测试这条路线了。在 Postman 中,从数据库或“获取所有帖子”API 中选择一个帖子 ID 后,向目标 API 发送 GET 请求:
接下来,我们从 MongoDB 验证这是否有效:
我们只保留了对象 ID,因为这样可以避免数据重复。如果您再次调用相同的 API,您会发现一些奇怪的事情,用户模型中的收藏夹里的帖子 ID 已经被移除了!
我们还在 Post 模型中保留了 favoriteCount。现在让我们来实现它。我们将把这段逻辑添加到 Postmodel 类中:
PostSchema.statics = {
createPost(args, user) {
...
},
list({ skip = 0, limit = 5 } = {}) {
...
},
incFavoriteCount(postId) {
return this.findByIdAndUpdate(postId, { $inc: { favoriteCount: 1 } });
},
decFavoriteCount(postId) {
return this.findByIdAndUpdate(postId, { $inc: { favoriteCount: -1 } });
}
};
incFavoriteCount 和 decFavoriteCount 方法首先使用 Mongo 的 findByIdAndUpdate 方法查找帖子 ID,然后使用 $inc 运算符,如果要增加收藏数,则加 1;如果要减少收藏数,则减 1。
现在我们也来修改 User 模型。首先添加以下导入语句:
import Post from '../posts/post.model';
然后,修改此处的 _favoritesmethod 功能:
\_favorites: {
async posts(postId) {
if (this.favorites.posts.indexOf(postId) >= 0) {
this.favorites.posts.remove(postId);
await Post.decFavoriteCount(postId);
} else {
this.favorites.posts.push(postId);
await Post.incFavoriteCount(postId);
}
return this.save();
}
}
Now the User model issue we stated above will resolve and the favoriteCount in Post model will also work:
If you hit the same API again and again, the result won’t change. Excellent! We have working APIs where a user can favorite a post as well.
A premium training course to learn to build apps with Node.js, Express, MongoDB, and friends. Start Learning Now →
Identifying if a Post is already a Favorite to User
the last section, we are able to register a new user in our application:
We are also able to allow a user to login into our application:
We were able to create a post related to a user:
Update a post:
And delete a Post as well:
In this section, we will be adding the following functionalities:
- We will send them if the current post is favorite to the user or not so that front-end can make decisions based on this fact
- We will make a route modification and work on Controller functions as well
Extending route
We just need to make very few modifications in our_post.route.js_file:
routes.get(‘/:id’, authJwt, postController.getPostById);
routes.get(‘/’, authJwt, postController.getPostsList);
We just added authJwt in these two existing lines. Once this is done, if I try to get Post list without Authorization header, we will get an error:
Extending the User model
Now, we will add more information to the post JSON if it is favorable to the current Authorizeduser.
Move to the _user.model.js_file and add this function in _favorites:
isPostIsFavorite(postId) {
if (this.favorites.posts.indexOf(postId) >= 0) {
return true;
}
return false;
}
Move to the _post.controller.js_file now and modify the getPostByIdfunction:
export async function getPostById(req, res) {
try {
const promise = await Promise.all([
User.findById(req.user.\_id),
Post.findById(req.params.id).populate('user')
]);
const favorite = promise[0].\_favorites.isPostIsFavorite(req.params.id);
const post = promise[1];
return res.status(HTTPStatus.OK).json({
...post.toJSON(),
favorite
});
} catch (e) {
return res.status(HTTPStatus.BAD\_REQUEST).json(e);
}
}
Here, we just added a new field favorite which will be reflected in a Post API like this:
We will modify our getPostsListfunction as well to include a Promise and return the appropriate response:
export async function getPostsList(req, res) {
const limit = parseInt(req.query.limit, 0);
const skip = parseInt(req.query.skip, 0);
try {
const promise = await Promise.all([
User.findById(req.user.\_id),
Post.list({ limit, skip })
]);
const posts = promise[1].reduce((arr, post) => {
const favorite = promise[0].\_favorites.isPostIsFavorite(post.\_id);
arr.push({
...post.toJSON(),
favorite
});
return arr;
}, []);
return res.status(HTTPStatus.OK).json(posts);
} catch (e) {
return res.status(HTTPStatus.BAD\_REQUEST).json(e);
}
}
Let’s run this now and get all posts:
Excellent.
Conclusion
your will learn a lot of Node and API knowledge from this post but has more and more topic that we should know eg.secrity, rate limit, best practice I hope you enjoy for this.
A premium training course to learn to build apps with Node.js, Express, MongoDB, and friends. Start Learning Now →
Disclaimer
This post contains affiliate links to products. We may receive a commission for purchases made through these links.
文章来源:https://dev.to/kris/building-rest-api-in-nodejs-mongodb-passport-jwt-4hbj
















