Express.js API 应用程序的极简架构模式
KO 建筑演示
Express.js 是一个极简的 Web 应用框架,旨在提高 Web 开发者的生产力。它非常灵活,并且不强制任何架构模式。本文演示了我设计的一种新的架构模式,它将进一步提高您的生产力。
如何阅读本文
本文介绍一种不同于流行的 MVC 或 MSC(模型-服务-控制器)模式的模式。您可以在了解任何这些模式之前阅读本文。
让我们创建一个餐厅应用程序 RESTful API。
访问规则
要求
每个餐厅对象必须有名称、地理位置坐标、可用状态和所有者 ID。
每个用户对象必须有名称、电子邮件、用户类型(成员/所有者/管理员)和密码。
此演示中的技术堆栈
JSON 响应约定
当我们将 JSON 数据发送回客户端时,我们可能会有识别操作成功或失败的约定,例如
{
success : false ,
error : ...
}
Enter fullscreen mode
Exit fullscreen mode
{
success : true ,
data : ...
}
Enter fullscreen mode
Exit fullscreen mode
让我们为上面的 JSON 响应创建函数。
./common/response.js
function errorRes ( res , err , errMsg = " failed operation " , statusCode = 500 ) {
console . error ( " ERROR: " , err )
return res . status ( statusCode ). json ({ success : false , error : errMsg })
}
function successRes ( res , data , statusCode = 200 ) {
return res . status ( statusCode ). json ({ success : true , data })
}
Enter fullscreen mode
Exit fullscreen mode
这里我们对两个函数都使用了默认参数,这样做的好处是我们可以这样使用该函数:
errorRes ( res , err )
successRes ( res , data )
Enter fullscreen mode
Exit fullscreen mode
我们不必检查可选参数是否为空。
// Example when default arguments not in use.
function errorRes ( res , err , errMsg , statusCode ) {
if ( errMsg ) {
if ( statusCode ) {
...
}
...
}
}
// or using ternary operator
function successRes ( res , data , statusCode ) {
const resStatusCode = statusCode ? statusCode : 200
...
}
Enter fullscreen mode
Exit fullscreen mode
请随意 console.error
使用您喜欢的日志记录功能(来自其他库)进行替换。
数据库异步回调约定
对于创建、读取、更新和删除操作,大多数数据库 ORM/驱动程序都有一个回调约定:
( err , data ) => ...
Enter fullscreen mode
Exit fullscreen mode
了解了这一点,让我们添加另一个函数 ./common/response.js
./common/response.js
function errData ( res , errMsg = " failed operation " ) {
return ( err , data ) => {
if ( err ) return errorRes ( res , err , errMsg )
return successRes ( res , data )
}
}
Enter fullscreen mode
Exit fullscreen mode
导出所有函数 ./common/response.js
module . exports = { errorRes , successRes , errData }
Enter fullscreen mode
Exit fullscreen mode
数据库操作(CRUD)约定
让我们为所有模型定义数据库操作函数。这里的约定是使用 req.body
作为数据源,并 req.params._id
使用 作为集合的对象 ID。大多数函数都会接受一个模型和一个填充字段列表作为参数,但删除操作除外(删除的记录无需填充)。由于 delete
是 JavaScript 中的保留关键字(用于从对象中删除属性),因此我们使用 remove
作为删除操作函数名以避免冲突。
./common/crud.js
const { errData , errorRes , successRes } = require ( ' ../common/response ' )
const mongoose = require ( ' mongoose ' )
function create ( model , populate = []) {
return ( req , res ) => {
const newData = new model ({
_id : new mongoose . Types . ObjectId (),
... req . body
})
return newData . save ()
. then ( t => t . populate (... populate , errData ( res )))
. catch ( err => errorRes ( res , err ))
}
}
function read ( model , populate = []) {
return ( req , res ) => (
model . find (... req . body , errData ( res )). populate (... populate )
)
}
function update ( model , populate = []) {
return ( req , res ) => {
req . body . updated_at = new Date ()
return model . findByIdAndUpdate (
req . params . _id ,
req . body ,
{ new : true },
errData ( res )
). populate (... populate )
}
}
function remove ( model ) {
return ( req , res ) => (
model . deleteOne ({ _id : req . params . _id }, errData ( res ))
)
}
module . exports = { read , create , update , remove }
Enter fullscreen mode
Exit fullscreen mode
上面的数据库 CRUD 函数使用了 中的函数 ./common/response
。
准备开发
定义完上述所有功能后,我们就可以进行应用程序开发了。现在我们只需要定义数据模型和路由器。 让我们在 ./models
./models/Restaurant.js
const mongoose = require ( ' mongoose ' )
const Schema = mongoose . Schema
const ObjectId = Schema . Types . ObjectId
const validator = require ( ' validator ' )
const restaurantSchema = new Schema ({
_id : ObjectId ,
name : { type : String , required : true },
location : {
type : {
type : String ,
enum : [ ' Point ' ],
required : true
},
coordinates : {
type : [ Number ],
required : true
}
},
owner : { type : ObjectId , ref : ' User ' , required : true },
available : {
type : Boolean ,
required : true ,
},
updated_at : Date ,
});
module . exports = mongoose . model ( ' Restaurant ' , restaurantSchema , ' restaurants ' );
Enter fullscreen mode
Exit fullscreen mode
./models/User.js
const mongoose = require ( ' mongoose ' )
const Schema = mongoose . Schema
const ObjectId = Schema . ObjectId
const validator = require ( ' validator ' )
const userSchema = new Schema ({
_id : ObjectId ,
name : { type : String , required : true },
email : {
type : String ,
required : true ,
unique : true ,
validate : [ validator . isEmail , ' invalid email ' ]
},
type : {
type : String ,
enum : [ ' member ' , ' owner ' , ' admin ' ],
required : true
},
password : { type : String , required : true , select : false },
updated_at : Date ,
});
module . exports = mongoose . model ( ' User ' , userSchema , ' users ' );
Enter fullscreen mode
Exit fullscreen mode
上面的模型非常常见,没有什么新意或花哨的东西。
路由和处理程序
从上面的数据库约定来看,你可能会觉得,如果需要后端处理 JSON 字段,使用 req.body 作为数据源会非常受限。这里我们可以使用中间件来解决这个限制。
./api/user.js
router
. use ( onlyAdmin )
. post ( ' / ' , create ( User ))
. get ( ' /all/:page ' , usersAtPage , read ( User ))
. put ( ' /:_id ' , handlePassword , update ( User ))
. delete ( ' /:_id ' , remove ( User ))
Enter fullscreen mode
Exit fullscreen mode
./api/restaurant.js
const express = require ( ' express ' )
const router = express . Router ()
const { create , read , update , remove } = require ( ' ../common/crud ' )
const Restaurant = require ( ' ../models/Restaurant ' )
router
. get ( ' /all/:lng/:lat/:page ' , nearBy (), read ( Restaurant , [ ' owner ' ]))
. get ( ' /available/:lng/:lat/:page ' ,
nearBy ({ available : true }),
read ( Restaurant , [ ' owner ' ])
)
function nearBy ( query = {}) {
return ( req , res , next ) => {
const { lng , lat , page } = req . params
req . body = geoQuery ( lng , lat , query , page )
next ()
}
}
Enter fullscreen mode
Exit fullscreen mode
./api/auth.js
router
. post ( ' /signup ' , isValidPassword , hashPassword , signUp )
. post ( ' /login ' , isValidPassword , findByEmail , verifyPassword , login )
// middlewares below are used for processing `password` field in `req.body`
function isValidPassword ( req , res , next ) {
const { password } = req . body
if ( ! password || password . length < 6 ) {
const err = `invalid password: ${ password } `
const errMsg = ' password is too short '
return errorRes ( res , err , errMsg )
}
return next ()
}
function hashPassword ( req , res , next ) {
const { password } = req . body
bcrypt . hash ( password , saltRounds , ( err , hashed ) => {
if ( err )
return errorRes ( res , err , ' unable to sign up, try again ' )
req . body . password = hashed
return next ()
})
}
function signUp ( req , res ) {
...
}
function findByEmail ( req , res , next ) {
....
}
function verifyPassword ( req , res , next ) {
...
}
function login ( req , res ) {
...
}
module . exports = router ;
Enter fullscreen mode
Exit fullscreen mode
如何扩展
扩展应用程序只需要添加新模型并为端点定义新的路由器。
与 MSC 的区别
模型-服务-控制器模式要求每个数据库模型都拥有一组用于数据操作的服务函数。这些服务函数仅针对特定模型进行定义。使用上述新架构,我们通过复用通用的数据库操作函数,无需为每个模型定义服务函数,从而提高了生产力。
概括
这种架构提供了极大的自定义灵活性,例如,它不强制使用文件夹结构 common
,只需拥有一个文件夹即可。您可以自由地将所有中间件函数放在路由器文件中,或根据规则进行分隔。通过使用和扩展文件夹中的函数 common
,您可以从头开始构建项目,也可以高效地重构/继续构建大型项目。到目前为止,我已经将此架构应用于各种规模的 ExpressJS 项目。
ExpressJS API 应用程序的极简架构模式
KO 建筑演示
框架:ExpressJS
数据库:MongoDB
身份验证:JSON Web 令牌
实验数据
API 文档
Postman API 集合和环境可以从 ./postman/
预运行
更新 ./config.js
文件
module . exports = {
saltRounds : 10 ,
jwtSecretSalt : '87908798' ,
devMongoUrl : 'mongodb://localhost/kane' ,
prodMongoUrl : 'mongodb://localhost/kane' ,
testMongoUrl : 'mongodb://localhost/test' ,
}
Enter fullscreen mode
Exit fullscreen mode
导入实验数据
打开终端并运行:
在此目录中打开另一个终端:
使用以下命令启动服务器
开始开发
鏂囩珷鏉ユ簮锛�https://dev.to/dividedbynil/a-minimalist-architecture-pattern-for-expressjs-api-applications-nee