为 Node.js API 设计更好的架构
设置数据库
创建控制器
创建路线
前段时间,我写了一篇关于使用 Node.js、React.js 和 MongoDB 创建全栈项目的文章。这是一个非常酷的入门项目,可以帮助我们快速上手并掌握基础知识。
但是,实现更好的架构非常重要,尤其是在你有一个大项目并且与一个大型团队合作的情况下。这将帮助你更轻松地开发和维护你的项目。
因此,这篇文章的目的是分享我当前的 API 架构以及我发现的创建更好的结构、应用设计模式和干净代码的方法。
让我们深入研究代码。
首先,让我们创建工作文件夹和初始文件。
$ mkdir node-starter
$ cd node-starter
$ touch index.js
$ npm init -y
创建结构
现在,让我们为项目创建基础文件夹
$ mkdir config src src/controllers src/models src/services src/helpers
添加依赖项
对于这个项目,我们将使用 Express 和 MongoDB,所以让我们添加我们的初始依赖项。
$ npm install --save body-parser express mongoose mongoose-unique-validator slugify
添加 DEV 依赖项
由于我们希望能够在该项目中使用最新的 ES6 语法,因此让我们添加 babel 并对其进行配置。
npm i -D @babel/node @babel/core @babel/preset-env babel-loader nodemon
在这里我们还添加了 nodemon 作为开发依赖项,以便轻松运行和测试项目。
设置 babel
在主文件夹中,创建一个名为 .babelrc 的文件,其中包含以下代码:
{
"presets": [
"@babel/preset-env"
]
}
现在转到你的 package.json 并添加以下脚本
"scripts": {
"start": "babel-node index.js",
"dev:start": "clear; nodemon --exec babel-node index.js"
}
创建服务器
在 config 文件夹下,创建一个名为 server.js 的文件,其中包含以下代码
import express from "express";
import bodyParser from "body-parser";
const server = express();
server.use(bodyParser.json());
export default server;
现在让我们将服务器配置导入到 index.js 文件中:
import server from './config/server';
const PORT = process.env.PORT || 5000;
server.listen(PORT, () => {
console.log(`app running on port ${PORT}`);
});
此时,您应该能够使用以下脚本运行您的服务器:
$ npm run dev:start
你应该会收到如下回复:
[nodemon] 1.19.4
[nodemon] to restart at any time, enter `rs`
[nodemon] watching dir(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `babel-node index.js`
app running on port 5000
设置数据库
现在让我们设置数据库。
为此,您必须在本地计算机上启动并运行 MongoDB。
在配置下,添加文件 database.js
//database.js
import mongoose from "mongoose";
class Connection {
constructor() {
const url =
process.env.MONGODB_URI || `mongodb://localhost:27017/node-starter`;
console.log("Establish new connection with url", url);
mongoose.Promise = global.Promise;
mongoose.set("useNewUrlParser", true);
mongoose.set("useFindAndModify", false);
mongoose.set("useCreateIndex", true);
mongoose.set("useUnifiedTopology", true);
mongoose.connect(url);
}
}
export default new Connection();
这里我们通过导出一个新的 Connection 来创建数据库的单例实例。当你像这样导出时,Node 会自动处理,并确保你的应用程序中只有一个此类的实例。
现在,将其导入到 index.js 文件的开头。
//index.js
import './config/database';
//...
创建模型
现在让我们创建第一个模型。
在 src/models 下,创建一个名为 Post.js 的文件,内容如下。
//src/models/Post.js
import mongoose, { Schema } from "mongoose";
import uniqueValidator from "mongoose-unique-validator";
import slugify from 'slugify';
class Post {
initSchema() {
const schema = new Schema({
title: {
type: String,
required: true,
},
slug: String,
subtitle: {
type: String,
required: false,
},
description: {
type: String,
required: false,
},
content: {
type: String,
required: true,
}
}, { timestamps: true });
schema.pre(
"save",
function(next) {
let post = this;
if (!post.isModified("title")) {
return next();
}
post.slug = slugify(post.title, "_");
console.log('set slug', post.slug);
return next();
},
function(err) {
next(err);
}
);
schema.plugin(uniqueValidator);
mongoose.model("posts", schema);
}
getInstance() {
this.initSchema();
return mongoose.model("posts");
}
}
export default Post;
创建我们的服务
让我们创建一个 Service 类,它将具有我们 API 的所有常见功能,以便其他服务可以继承它们。
在 src/services 文件夹下创建一个 Service.js 文件;
//src/services/Service.js
import mongoose from "mongoose";
class Service {
constructor(model) {
this.model = model;
this.getAll = this.getAll.bind(this);
this.insert = this.insert.bind(this);
this.update = this.update.bind(this);
this.delete = this.delete.bind(this);
}
async getAll(query) {
let { skip, limit } = query;
skip = skip ? Number(skip) : 0;
limit = limit ? Number(limit) : 10;
delete query.skip;
delete query.limit;
if (query._id) {
try {
query._id = new mongoose.mongo.ObjectId(query._id);
} catch (error) {
console.log("not able to generate mongoose id with content", query._id);
}
}
try {
let items = await this.model
.find(query)
.skip(skip)
.limit(limit);
let total = await this.model.count();
return {
error: false,
statusCode: 200,
data: items,
total
};
} catch (errors) {
return {
error: true,
statusCode: 500,
errors
};
}
}
async insert(data) {
try {
let item = await this.model.create(data);
if (item)
return {
error: false,
item
};
} catch (error) {
console.log("error", error);
return {
error: true,
statusCode: 500,
message: error.errmsg || "Not able to create item",
errors: error.errors
};
}
}
async update(id, data) {
try {
let item = await this.model.findByIdAndUpdate(id, data, { new: true });
return {
error: false,
statusCode: 202,
item
};
} catch (error) {
return {
error: true,
statusCode: 500,
error
};
}
}
async delete(id) {
try {
let item = await this.model.findByIdAndDelete(id);
if (!item)
return {
error: true,
statusCode: 404,
message: "item not found"
};
return {
error: false,
deleted: true,
statusCode: 202,
item
};
} catch (error) {
return {
error: true,
statusCode: 500,
error
};
}
}
}
export default Service;
好的,这看起来有很多代码。
在此服务中,我们为我们的应用程序创建了主要功能(基本 CRUD),添加了获取、插入、更新和删除项目的功能。
现在,让我们创建 Post 服务并继承我们刚刚创建的所有功能。
在 src/services 目录下,创建一个 PostService.js 文件,内容如下:
//src/services/PostService
import Service from './Service';
class PostService extends Service {
constructor(model) {
super(model);
}
};
export default PostService;
它就是这么简单,它继承了我们在主 Service.js 文件中创建的所有功能,并且可以跨 API 为所有其他端点重复使用。
创建控制器
我们将遵循创建服务时相同的原则,在这里我们将创建一个主 Controller.js 文件,它将具有所有常见功能并使其他控制器继承它。
在src/controllers下创建文件Controller.js,并添加以下代码:
//src/controllers/Controller.js
class Controller {
constructor(service) {
this.service = service;
this.getAll = this.getAll.bind(this);
this.insert = this.insert.bind(this);
this.update = this.update.bind(this);
this.delete = this.delete.bind(this);
}
async getAll(req, res) {
return res.status(200).send(await this.service.getAll(req.query));
}
async insert(req, res) {
let response = await this.service.insert(req.body);
if (response.error) return res.status(response.statusCode).send(response);
return res.status(201).send(response);
}
async update(req, res) {
const { id } = req.params;
let response = await this.service.update(id, req.body);
return res.status(response.statusCode).send(response);
}
async delete(req, res) {
const { id } = req.params;
let response = await this.service.delete(id);
return res.status(response.statusCode).send(response);
}
}
export default Controller;
现在,让我们在 src/controllers 下创建一个 PostController 文件
//src/controllers/PostController.js
import Controller from './Controller';
import PostService from "./../services/PostService";
import Post from "./../models/Post";
const postService = new PostService(
new Post().getInstance()
);
class PostController extends Controller {
constructor(service) {
super(service);
}
}
export default new PostController(postService);
在这里,我们导入所需的服务和模型,并且我们还创建了 Post 服务的实例,并将 Post 模型实例传递给其构造函数。
创建路线
现在是时候为我们的 API 创建路由了。
在config文件夹下,创建文件routes.js
//config/routes.js
import PostController from './../src/controllers/PostController';
export default (server) => {
// POST ROUTES
server.get(`/api/post`, PostController.getAll);
server.post(`/api/post`, PostController.insert)
server.put(`/api/post/:id`, PostController.update);
server.delete(`/api/post/:id`, PostController.delete);
}
该文件导入 Post 控制器并将功能映射到所需的路由。
现在我们必须在设置主体解析器之后立即将路由导入到 server.js 文件中,如下所示:
//config/server.js
//...
import setRoutes from "./routes";
setRoutes(server);
//...
此时,您应该能够向所有创建的路线发出请求,因此让我们测试一下。
使用以下 json 主体对路由 /api/post 发出 POST 请求:在这里,您可以使用Postman或Insomnia
等 API 客户端来完成此任务
{
"title": "post 1",
"subtitle": "subtitle post 1",
"content": "content post 1"
}
你应该得到如下结果:
{
"error": false,
"item": {
"_id": "5dbdea2e188d860cf3bd07d1",
"title": "post 1",
"subtitle": "subtitle post 1",
"content": "content post 1",
"createdAt": "2019-11-02T20:42:22.339Z",
"updatedAt": "2019-11-02T20:42:22.339Z",
"slug": "post_1",
"__v": 0
}
}
结论
设计 API 架构的方法有很多,其目标始终是拥有更清晰、可重用的代码,不重复自己并帮助其他人轻松地协同工作,此外它还可以帮助您进行维护和添加新功能。
您可以在这里找到源代码
希望对你有用。
再见!