为 Node.js API 设计更好的架构设置数据库创建控制器创建路由

2025-05-26

为 Node.js API 设计更好的架构

设置数据库

创建控制器

创建路线

前段时间,我写了一篇关于使用 Node.js、React.js 和 MongoDB 创建全栈项目的文章。这是一个非常酷的入门项目,可以帮助我们快速上手并掌握基础知识。

但是,实现更好的架构非常重要,尤其是在你有一个大项目并且与一个大型团队合作的情况下。这将帮助你更轻松地开发和维护你的项目。

因此,这篇文章的目的是分享我当前的 API 架构以及我发现的创建更好的结构、应用设计模式和干净代码的方法。

让我们深入研究代码。

首先,让我们创建工作文件夹和初始文件。

$ mkdir node-starter
$ cd node-starter
$ touch index.js
$ npm init -y
Enter fullscreen mode Exit fullscreen mode

创建结构

现在,让我们为项目创建基础文件夹

 $ mkdir config src src/controllers src/models src/services src/helpers
Enter fullscreen mode Exit fullscreen mode

添加依赖项

对于这个项目,我们将使用 Express 和 MongoDB,所以让我们添加我们的初始依赖项。

$ npm install --save body-parser express mongoose mongoose-unique-validator slugify
Enter fullscreen mode Exit fullscreen mode

添加 DEV 依赖项

由于我们希望能够在该项目中使用最新的 ES6 语法,因此让我们添加 babel 并对其进行配置。

npm i -D @babel/node @babel/core @babel/preset-env babel-loader nodemon
Enter fullscreen mode Exit fullscreen mode

在这里我们还添加了 nodemon 作为开发依赖项,以便轻松运行和测试项目。

设置 babel

在主文件夹中,创建一个名为 .babelrc 的文件,其中包含以下代码:

{
  "presets": [
    "@babel/preset-env"
  ]
}
Enter fullscreen mode Exit fullscreen mode

现在转到你的 package.json 并添加以下脚本

"scripts": {
    "start": "babel-node index.js",
    "dev:start": "clear; nodemon --exec babel-node index.js"
 }
Enter fullscreen mode Exit fullscreen mode

创建服务器

在 config 文件夹下,创建一个名为 server.js 的文件,其中包含以下代码

import express from "express";
import bodyParser from "body-parser";
const server = express();

server.use(bodyParser.json());

export default server;
Enter fullscreen mode Exit fullscreen mode

现在让我们将服务器配置导入到 index.js 文件中:

import server from './config/server';

const PORT = process.env.PORT || 5000;
server.listen(PORT, () => {
  console.log(`app running on port ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

此时,您应该能够使用以下脚本运行您的服务器:

$ npm run dev:start
Enter fullscreen mode Exit fullscreen mode

你应该会收到如下回复:

[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
Enter fullscreen mode Exit fullscreen mode

设置数据库

现在让我们设置数据库。
为此,您必须在本地计算机上启动并运行 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();


Enter fullscreen mode Exit fullscreen mode

这里我们通过导出一个新的 Connection 来创建数据库的单例实例。当你像这样导出时,Node 会自动处理,并确保你的应用程序中只有一个此类的实例。

现在,将其导入到 index.js 文件的开头。

//index.js
import './config/database';
//...
Enter fullscreen mode Exit fullscreen mode

创建模型

现在让我们创建第一个模型。
在 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;

Enter fullscreen mode Exit fullscreen mode

创建我们的服务

让我们创建一个 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;

Enter fullscreen mode Exit fullscreen mode

好的,这看起来有很多代码。

在此服务中,我们为我们的应用程序创建了主要功能(基本 CRUD),添加了获取、插入、更新和删除项目的功能。

现在,让我们创建 Post 服务并继承我们刚刚创建的所有功能。
在 src/services 目录下,创建一个 PostService.js 文件,内容如下:

//src/services/PostService
import Service from './Service';

class PostService extends Service {
  constructor(model) {
    super(model);
  }
};

export default PostService;


Enter fullscreen mode Exit fullscreen mode

它就是这么简单,它继承了我们在主 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;
Enter fullscreen mode Exit fullscreen mode

现在,让我们在 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);

Enter fullscreen mode Exit fullscreen mode

在这里,我们导入所需的服务和模型,并且我们还创建了 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);

}
Enter fullscreen mode Exit fullscreen mode

该文件导入 Post 控制器并将功能映射到所需的路由。

现在我们必须在设置主体解析器之后立即将路由导入到 server.js 文件中,如下所示:

//config/server.js
//...
import setRoutes from "./routes";
setRoutes(server);
//...
Enter fullscreen mode Exit fullscreen mode

瞧!

此时,您应该能够向所有创建的路线发出请求,因此让我们测试一下。

使用以下 json 主体对路由 /api/post 发出 POST 请求:在这里,您可以使用PostmanInsomnia
等 API 客户端来完成此任务

{
    "title": "post 1",
    "subtitle": "subtitle post 1",
    "content": "content post 1"
}
Enter fullscreen mode Exit fullscreen mode

你应该得到如下结果:

{
  "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
  }
}
Enter fullscreen mode Exit fullscreen mode

结论

设计 API 架构的方法有很多,其目标始终是拥有更清晰、可重用的代码,不重复自己并帮助其他人轻松地协同工作,此外它还可以帮助您进行维护和添加新功能。

您可以在这里找到源代码

希望对你有用。
再见!

文章来源:https://dev.to/pacheco/designing-a-better-architecture-for-a-node-js-api-24d
PREV
全栈设置(Node.js、React.js 和 MongoDB)
NEXT
从头开始使用 Golang 创建 Restful API