Express.js API 应用程序的极简架构模式 KO 架构演示

2025-06-08

Express.js API 应用程序的极简架构模式

KO 建筑演示

Express.js 是一个极简的 Web 应用框架,旨在提高 Web 开发者的生产力。它非常灵活,并且不强制任何架构模式。本文演示了我设计的一种新的架构模式,它将进一步提高您的生产力。

如何阅读本文

本文介绍一种不同于流行的 MVC 或 MSC(模型-服务-控制器)模式的模式。您可以在了解任何这些模式之前阅读本文。

演示项目GitHub

让我们创建一个餐厅应用程序 RESTful API。

访问规则

  • 公共用户:
    • 创建一个帐户
    • 登录
  • 成员:
    • 阅读附近所有可用的餐厅
  • 业主:
    • CRUD 附近所有餐厅
  • 管理员:
    • CRUD 附近所有餐厅
    • CRUD 所有用户

要求

  • 每个餐厅对象必须有名称、地理位置坐标、可用状态和所有者 ID。
  • 每个用户对象必须有名称、电子邮件、用户类型(成员/所有者/管理员)和密码。

此演示中的技术堆栈

  • 数据库:MongoDB
  • ORM:Mongoose

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 项目。

GitHub 徽标 dividedbynil / ko-architecture

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

导入实验数据

打开终端并运行:

mongod

在此目录中打开另一个终端:

bash ./data/import.sh

使用以下命令启动服务器

npm start

开始开发

npm run dev
鏂囩珷鏉ユ簮锛�https://dev.to/dividedbynil/a-minimalist-architecture-pattern-for-expressjs-api-applications-nee
PREV
Python 和 JavaScript 中的等效方法。第一部分
NEXT
OpenCommit:使用 GPT 🤯🔫 (v 2.0) 在 1 秒内生成令人印象深刻的提交的 CLI GenAI LIVE!| 2025 年 6 月 4 日