如何使用 NodeJS、Express 和 MySQL 构建 Rest API

2025-06-07

如何使用 NodeJS、Express 和 MySQL 构建 Rest API

有了 JavaScript 和 MySQL 的知识,我们可以使用 Express 构建我们的 NodeJS API。

我做了一些研究,并尝试从头开始开发一个 API。
我喜欢简化流程,并尽量避免代码重复。

本指南将向您展示如何从头构建 API:
您将学习如何创建路由、
如何使用 mysql2、如何配置和连接数据库以及如何使用预处理语句运行查询。
如何创建一个除了 req、res 和 next 回调之外还可以获取其他参数的中间件。
您将学习如何使用 Express Validator 模块检查请求对象中的数据。
您将学习如何使用 JWT 模块为用户创建令牌、验证令牌以及获取存储在令牌中的对象。
此外,您还将学习如何根据用户角色授予他们访问特定路由的权限。

技术和封装:

  • NodeJS
  • 表达
  • mysql2
  • bcryptjs
  • jsonwebtoken
  • express-validator
  • dotenv
  • 科尔斯

安装 MySQL:

我使用 WSL,您可以使用本教程了解如何在 WSL 中安装 MySQL。
您需要使用以下命令确保 MySQL 正在运行:



sudo service mysql status


Enter fullscreen mode Exit fullscreen mode

如果它没有运行,只需使用:



sudo service mysql start


Enter fullscreen mode Exit fullscreen mode

应用概述:

我们将构建一个用于 CRUD 操作的 REST API:创建、读取、更新和删除用户。



+---------+------------------------------+--------------------------------+
| Methods |             Urls             |            Actions             |
+---------+------------------------------+--------------------------------+
| Get     | /api/v1/users                | Get all users                  |
| Get     | /api/v1/users/id/1           | Get user with id=1             |
| Get     | /api/v1/users/username/julia | Get user with username='julia' |
| Get     | /api/v1/users/whoami         | Get the current user details   |
| Post    | /api/v1/users                | Create new user                |
| Patch   | /api/v1/users/users/id/1     | Update user with id=1          |
| Delete  | /api/v1/users/id/1           | Delete user with id=1          |
| Post    | /api/v1/users/login          | Login with email and password  |
+---------+------------------------------+--------------------------------+



Enter fullscreen mode Exit fullscreen mode

创建项目文件夹并安装所有依赖项:



mkdir mysql-node-express && cd mysql-node-express
npm init -y
npm i express express-validator mysql2 cors dotenv jsonwebtoken -S
npm i nodemon -D


Enter fullscreen mode Exit fullscreen mode

转到 package.json 文件并将“main”值更改为“src/server.js”,并将这些脚本添加到脚本对象:



"start": "node src/server.js",
"dev": "nodemon"


Enter fullscreen mode Exit fullscreen mode

package.json 应该如下所示:



{
  "name": "mysql-node-express",
  "version": "1.0.0",
  "description": "",
  "main": "src/server.js",
  "scripts": {
    "start": "node src/server.js",
    "dev": "nodemon"
  },
  "author": "Julia Strichash",
  "license": "ISC",
  "dependencies": {
    "bcryptjs": "^2.4.3",
    "cors": "^2.8.5",
    "dotenv": "^8.2.0",
    "express": "^4.17.1",
    "express-validator": "^6.6.0",
    "jsonwebtoken": "^8.5.1",
    "mysql2": "^2.1.0"
  },
  "devDependencies": {
    "nodemon": "^2.0.4"
  }
}



Enter fullscreen mode Exit fullscreen mode

创建 .env 文件:

我们将使用 .env 文件来管理所有环境变量。.env
文件是一个隐藏文件,允许我们使用 ENV VARIABLE = VALUE 语法自定义环境变量。
这些变量使用我们已安装的 dotenv 模块加载。.env
文件可以在环境的不同阶段(开发 / 阶段 / 生产环境)进行定义。

创建 .env 文件,复制以下行,然后使用您的 MySQL db_name、db_username 和密码更新文件:



# DB Configurations
HOST=localhost
DB_USER=db_username
DB_PASS=db_password
DB_DATABASE=db_name


# local runtime configs
PORT=3000
SECRET_JWT=supersecret


Enter fullscreen mode Exit fullscreen mode

创建 nodemon.json 文件:

Nodemon 是一个基于 Node.js 开发应用程序的工具,当检测到目标目录中的文件更改时,它会自动重启 Node 应用程序。Nodemon
是 Node 的一个替代包装器。我们不应该使用 Node 命令,而应该在命令行中使用 Nodemon 命令来执行脚本。
在命令行上运行 Nodemon 时,我们可以轻松添加配置开关,例如:



nodemon --watch src


Enter fullscreen mode Exit fullscreen mode

我们还可以使用一个文件 (nodemon.json) 来指定所有开关。
如果我们想监视目录中的多个文件,可以将该目录添加到“watch”数组中。
如果我们想搜索特定扩展名(例如 ts 文件),可以使用“ext”属性。
如果我们想忽略某些文件,可以在“ignore”数组中定义它们,等等……
我主要在基于 TypeScript 的 NodeJS 服务器中使用这个文件,但我认为有更多地方包含我们的应用配置会更容易。
这个文件是可选的。

创建 nodemon.json 文件并将其添加到文件中:



{
    "watch": ["src"],
    "ext": ".js",
    "ignore": []
  }


Enter fullscreen mode Exit fullscreen mode

创建 src 文件夹:



mkdir src && cd src


Enter fullscreen mode Exit fullscreen mode

在 src 文件夹中创建子文件夹:控制器、模型、路由、中间件、数据库和实用程序:



mkdir controllers models routes middleware db utils


Enter fullscreen mode Exit fullscreen mode

该项目应该是这样的

设置 Express 服务器:

在 src 目录中创建文件 server.js 并复制以下行:



const express = require("express");
const dotenv = require('dotenv');
const cors = require("cors");
const HttpException = require('./utils/HttpException.utils');
const errorMiddleware = require('./middleware/error.middleware');
const userRouter = require('./routes/user.route');

// Init express
const app = express();
// Init environment
dotenv.config();
// parse requests of content-type: application/json
// parses incoming requests with JSON payloads
app.use(express.json());
// enabling cors for all requests by using cors middleware
app.use(cors());
// Enable pre-flight
app.options("*", cors());

const port = Number(process.env.PORT || 3331);

app.use(`/api/v1/users`, userRouter);

// 404 error
app.all('*', (req, res, next) => {
    const err = new HttpException(404, 'Endpoint Not Found');
    next(err);
});

// Error middleware
app.use(errorMiddleware);

// starting the server
app.listen(port, () =>
    console.log(`🚀 Server running on port ${port}!`));


module.exports = app;


Enter fullscreen mode Exit fullscreen mode

在此文件中,我们导入 express 来构建其余 API,并使用 express.json() 来解析带有 JSON 有效负载的传入请求。

我们还导入 dotenv 模块来读取 .env 配置文件以获取运行服务器的端口号。

Cors 用于允许跨站点 HTTP 请求,在本例中,通过使用通配符 *,它允许从任何来源(任何域)进行访问。在使用路由之前,我们将调用 app.use(cors)); 。

我们还导入了 userRouter。

之后,我们有一个处理 404 错误的中间件 → 如果有人查找不存在的端点,他们将收到此错误:“未找到端点”,状态码为 404。之后,我们使用错误中间件,它将从之前的路由中获取错误数据。如果调用 next(err) ,您可以看到 404 中间件作为示例。
我们从 .env 文件中监听端口,并将其打印到服务器正在运行的控制台上。
创建 server.js 之后

创建MySQL数据库和用户表:

在 db 目录中,我们将创建 create-user-db.sql 文件并复制粘贴以下行:



DROP DATABASE IF EXISTS test_db;   
CREATE DATABASE IF NOT EXISTS test_db;   
USE test_db; 

DROP TABLE IF EXISTS user; 

CREATE TABLE IF NOT EXISTS user 
  ( 
     id         INT PRIMARY KEY auto_increment, 
     username   VARCHAR(25) UNIQUE NOT NULL, 
     password   CHAR(60) NOT NULL, 
     first_name VARCHAR(50) NOT NULL, 
     last_name  VARCHAR(50) NOT NULL, 
     email      VARCHAR(100) UNIQUE NOT NULL, 
     role       ENUM('Admin', 'SuperUser') DEFAULT 'SuperUser', 
     age        INT(11) DEFAULT 0 
  ); 


Enter fullscreen mode Exit fullscreen mode

在此脚本中,我们首先删除数据库(如果存在),以便在发生错误时快速重置(您可以根据需要注释掉该行);然后,如果数据库不存在,我们将创建数据库。我们将其设置为活动数据库,并创建一个包含所有列(id、username 等)的“user”表,以便在需要时方便地重置。如果您正在使用数据库客户端,则可以在其中运行此查询。

如果您使用的是 wsl,则可以在 db 目录中运行:



mysql -u [db_username] -p[db_password] < create-user-db.sql



Enter fullscreen mode Exit fullscreen mode

配置并连接到 MySQL 数据库:

在 db 目录中创建一个附加文件,名为 db-connection.js,然后复制粘贴以下内容:



const dotenv = require('dotenv');
dotenv.config();
const mysql2 = require('mysql2');

class DBConnection {
    constructor() {
        this.db = mysql2.createPool({
            host: process.env.DB_HOST,
            user: process.env.DB_USER,
            password: process.env.DB_PASS,
            database: process.env.DB_DATABASE
        });

        this.checkConnection();
    }

    checkConnection() {
        this.db.getConnection((err, connection) => {
            if (err) {
                if (err.code === 'PROTOCOL_CONNECTION_LOST') {
                    console.error('Database connection was closed.');
                }
                if (err.code === 'ER_CON_COUNT_ERROR') {
                    console.error('Database has too many connections.');
                }
                if (err.code === 'ECONNREFUSED') {
                    console.error('Database connection was refused.');
                }
            }
            if (connection) {
                connection.release();
            }
            return
        });
    }

    query = async (sql, values) => {
        return new Promise((resolve, reject) => {
            const callback = (error, result) => {
                if (error) {
                    reject(error);
                    return;
                }
                resolve(result);
            }
            // execute will internally call prepare and query
            this.db.execute(sql, values, callback);
        }).catch(err => {
            const mysqlErrorList = Object.keys(HttpStatusCodes);
            // convert mysql errors which in the mysqlErrorList list to http status code
            err.status = mysqlErrorList.includes(err.code) ? HttpStatusCodes[err.code] : err.status;

            throw err;
        });
    }
}

// like ENUM
const HttpStatusCodes = Object.freeze({
    ER_TRUNCATED_WRONG_VALUE_FOR_FIELD: 422,
    ER_DUP_ENTRY: 409
});


module.exports = new DBConnection().query;


Enter fullscreen mode Exit fullscreen mode

在这个文件中,我们首先导入 dotenv 模块并从 .env 文件中读取数据库配置信息,如数据库主机、数据库用户。

我们检查连接,以防数据库出现问题,然后释放连接。

我们有一个查询方法,它返回查询结果的承诺。

我们使用 try-catch 块来捕获常见的 MySQL 错误并返回适当的 HTTP 状态代码和消息。

在文件末尾,我们创建 DBConnection 类的实例并使用查询方法,而在 model.js(我们将在下一步中看到)中,我们将再次使用查询方法。

创建错误处理程序:

接下来,我们将创建错误处理程序。

为此,首先,我们将在 utils 目录下创建 HttpException.utils.js 文件,然后复制粘贴以下内容:



class HttpException extends Error {
    constructor(status, message, data) {
        super(message);
        this.status = status;
        this.message = message;
        this.data = data;
    }
}

module.exports = HttpException;


Enter fullscreen mode Exit fullscreen mode

HttpException 类继承自 Error 类。
其构造函数将获取状态、消息和数据。我们将使用 super(message) 将消息变量传递给父构造函数,然后初始化状态、消息和数据实例变量。

之后,我们将在中间件目录中创建一个中间件错误处理程序。
我们将创建一个 error.middleware.js 文件并复制粘贴以下内容:



function errorMiddleware(error, req, res, next) {
    let { status = 500, message, data } = error;

    console.log(`[Error] ${error}`);

    // If status code is 500 - change the message to Intrnal server error
    message = status === 500 || !message ? 'Internal server error' : message;

    error = {
        type: 'error',
        status,
        message,
        ...(data) && data
    }

    res.status(status).send(error);
}

module.exports = errorMiddleware;
/*
{
    type: 'error',
    status: 404,
    message: 'Not Found'
    data: {...} // optional
}
*/


Enter fullscreen mode Exit fullscreen mode

我们可以在文件底部看到对象将会是什么样子。

中间件将获得 req、res 和 next 回调,但它还将获得一个附加参数 error(通过在到达该中间件之前使用 next(error))。

我们使用解构从错误对象中获取变量,如果之前没有配置过,则将状态设置为 500。

在此之后,无论状态是否为 500,我们都会确保更改消息,以便用户收到通用的内部服务器错误消息,而不会透露故障的确切性质。

之后,我们创建一个具有类型、状态和消息属性(数据是可选的)的错误对象。
该项目应该是这样的

创建实用程序(帮助程序)文件:

在 utils 目录中,我们再创建两个文件,common.utils.js 和 userRoles.utils.js。

common.utils.js:



exports.multipleColumnSet = (object) => {
    if (typeof object !== 'object') {
        throw new Error('Invalid input');
    }

    const keys = Object.keys(object);
    const values = Object.values(object);

    columnSet = keys.map(key => `${key} = ?`).join(', ');

    return {
        columnSet,
        values
    }
}


Enter fullscreen mode Exit fullscreen mode

此函数用于为已准备好的查询设置多个字段,这些字段包含键值对。ColumnSet
是 key =? 对的数组,
因此,这些值的顺序应与 columnSet 数组的顺序相同。

用户角色.utils.js:



module.exports = {
    Admin: 'Admin',
    SuperUser: 'SuperUser'
}


Enter fullscreen mode Exit fullscreen mode

utils 目录

创建异步函数:

在中间件目录中创建另一个名为 awaitHandlerFactory.middleware.js 的文件并复制粘贴以下内容:



const awaitHandlerFactory = (middleware) => {
    return async (req, res, next) => {
        try {
            await middleware(req, res, next)
        } catch (err) {
            next(err)
        }
    }
}

module.exports = awaitHandlerFactory;


Enter fullscreen mode Exit fullscreen mode

一般来说,我们知道中间件只是一种获取 req、res 和 next 参数的异步方法,因此,如果我们希望这个中间件获取一个额外的参数,我们会这样做(下一步我们也会在 auth 中间件中使用它)。

此函数将获取一个回调,运行中间件脚本,并尝试在 try 代码块中触发此回调。
如果这里出现错误,它将捕获错误,然后我们将使用 next(err) (它将错误转移到下一个中​​间件 => error.middleware.js)。

创建身份验证中间件:

我们需要的另一个中间件是 auth 中间件,我们将使用它通过 JWT 模块检查用户权限。



const HttpException = require('../utils/HttpException.utils');
const UserModel = require('../models/user.model');
const jwt = require('jsonwebtoken');
const dotenv = require('dotenv');
dotenv.config();

const auth = (...roles) => {
    return async function (req, res, next) {
        try {
            const authHeader = req.headers.authorization;
            const bearer = 'Bearer ';

            if (!authHeader || !authHeader.startsWith(bearer)) {
                throw new HttpException(401, 'Access denied. No credentials sent!');
            }

            const token = authHeader.replace(bearer, '');
            const secretKey = process.env.SECRET_JWT || "";

            // Verify Token
            const decoded = jwt.verify(token, secretKey);
            const user = await UserModel.findOne({ id: decoded.user_id });

            if (!user) {
                throw new HttpException(401, 'Authentication failed!');
            }

            // check if the current user is the owner user
            const ownerAuthorized = req.params.id == user.id;

            // if the current user is not the owner and
            // if the user role don't have the permission to do this action.
            // the user will get this error
            if (!ownerAuthorized && roles.length && !roles.includes(user.role)) {
                throw new HttpException(401, 'Unauthorized');
            }

            // if the user has permissions
            req.currentUser = user;
            next();

        } catch (e) {
            e.status = 401;
            next(e);
        }
    }
}

module.exports = auth;


Enter fullscreen mode Exit fullscreen mode

与 awaitHandlerFactory.middleware.js 中间件类似,我们这里有一个中间件,需要额外的参数(可选)=> 角色。

我使用try-catch将catch区域的错误状态调整为401(例如,如果令牌已过期)。

首先,我们查找 req.headers.authorization —— 无论它是否在标头中定义,或者标头是否以“Bearer”开头,用户都会收到 401 响应。如果它以“Bearer”开头,我们将获取令牌并使用 .env 文件中的密钥对其进行解密。

我们将使用 jwt.verify 同步函数来验证令牌。该函数接收令牌和 secretKey 作为参数,并返回解码后的有效负载,以检查签名是否有效,以及可选的 expiration、audience 或 issuer 字段是否有效。否则将抛出错误。

现在,我们可以通过搜索用户 ID 来找到拥有此令牌的用户。
如果用户不再存在,则会抛出 401 异常,且不包含任何信息。
如果用户存在,我们将检查当前用户是否是正在搜索其路由的所有者,或者用户是否拥有访问此路由的角色。
我们保存当前用户信息是为了方便他在下一个中间件(例如“whoami”路由)中获取其数据。

使用 Express Validator 模块进行数据验证:

在中间件目录中,我们将创建一个附加文件,用于验证 req.body 属性。

在中间件目录中创建一个名为 validators 的子文件夹,并在该目录中创建一个文件 userValidator.middleware.js。复制粘贴以下内容:



const { body } = require('express-validator');
const Role = require('../../utils/userRoles.utils');


exports.createUserSchema = [
    body('username')
        .exists()
        .withMessage('username is required')
        .isLength({ min: 3 })
        .withMessage('Must be at least 3 chars long'),
    body('first_name')
        .exists()
        .withMessage('Your first name is required')
        .isAlpha()
        .withMessage('Must be only alphabetical chars')
        .isLength({ min: 3 })
        .withMessage('Must be at least 3 chars long'),
    body('last_name')
        .exists()
        .withMessage('Your last name is required')
        .isAlpha()
        .withMessage('Must be only alphabetical chars')
        .isLength({ min: 3 })
        .withMessage('Must be at least 3 chars long'),
    body('email')
        .exists()
        .withMessage('Email is required')
        .isEmail()
        .withMessage('Must be a valid email')
        .normalizeEmail(),
    body('role')
        .optional()
        .isIn([Role.Admin, Role.SuperUser])
        .withMessage('Invalid Role type'),
    body('password')
        .exists()
        .withMessage('Password is required')
        .notEmpty()
        .isLength({ min: 6 })
        .withMessage('Password must contain at least 6 characters')
        .isLength({ max: 10 })
        .withMessage('Password can contain max 10 characters'),
    body('confirm_password')
        .exists()
        .custom((value, { req }) => value === req.body.password)
        .withMessage('confirm_password field must have the same value as the password field'),
    body('age')
        .optional()
        .isNumeric()
        .withMessage('Must be a number')
];

exports.updateUserSchema = [
    body('username')
        .optional()
        .isLength({ min: 3 })
        .withMessage('Must be at least 3 chars long'),
    body('first_name')
        .optional()
        .isAlpha()
        .withMessage('Must be only alphabetical chars')
        .isLength({ min: 3 })
        .withMessage('Must be at least 3 chars long'),
    body('last_name')
        .optional()
        .isAlpha()
        .withMessage('Must be only alphabetical chars')
        .isLength({ min: 3 })
        .withMessage('Must be at least 3 chars long'),
    body('email')
        .optional()
        .isEmail()
        .withMessage('Must be a valid email')
        .normalizeEmail(),
    body('role')
        .optional()
        .isIn([Role.Admin, Role.SuperUser])
        .withMessage('Invalid Role type'),
    body('password')
        .optional()
        .notEmpty()
        .isLength({ min: 6 })
        .withMessage('Password must contain at least 6 characters')
        .isLength({ max: 10 })
        .withMessage('Password can contain max 10 characters')
        .custom((value, { req }) => !!req.body.confirm_password)
        .withMessage('Please confirm your password'),
    body('confirm_password')
        .optional()
        .custom((value, { req }) => value === req.body.password)
        .withMessage('confirm_password field must have the same value as the password field'),
    body('age')
        .optional()
        .isNumeric()
        .withMessage('Must be a number'),
    body()
        .custom(value => {
            return !!Object.keys(value).length;
        })
        .withMessage('Please provide required field to update')
        .custom(value => {
            const updates = Object.keys(value);
            const allowUpdates = ['username', 'password', 'confirm_password', 'email', 'role', 'first_name', 'last_name', 'age'];
            return updates.every(update => allowUpdates.includes(update));
        })
        .withMessage('Invalid updates!')
];

exports.validateLogin = [
    body('email')
        .exists()
        .withMessage('Email is required')
        .isEmail()
        .withMessage('Must be a valid email')
        .normalizeEmail(),
    body('password')
        .exists()
        .withMessage('Password is required')
        .notEmpty()
        .withMessage('Password must be filled')
];


Enter fullscreen mode Exit fullscreen mode

在这个文件中,我使用了 express-validator 模块,当我们需要检查某些属性、检查属性是否存在,或者如果任何属性值无效则创建自定义检查并向用户发送自定义消息时,该模块非常容易使用。
中间件目录

现在我们可以开始创建我们的路线、控制器和模型文件。

定义路线:

在 routes 目录中创建 user.route.js 文件并复制粘贴以下内容:



const express = require('express');
const router = express.Router();
const userController = require('../controllers/user.controller');
const auth = require('../middleware/auth.middleware');
const Role = require('../utils/userRoles.utils');
const awaitHandlerFactory = require('../middleware/awaitHandlerFactory.middleware');

const { createUserSchema, updateUserSchema, validateLogin } = require('../middleware/validators/userValidator.middleware');


router.get('/', auth(), awaitHandlerFactory(userController.getAllUsers)); // localhost:3000/api/v1/users
router.get('/id/:id', auth(), awaitHandlerFactory(userController.getUserById)); // localhost:3000/api/v1/users/id/1
router.get('/username/:username', auth(), awaitHandlerFactory(userController.getUserByuserName)); // localhost:3000/api/v1/users/usersname/julia
router.get('/whoami', auth(), awaitHandlerFactory(userController.getCurrentUser)); // localhost:3000/api/v1/users/whoami
router.post('/', createUserSchema, awaitHandlerFactory(userController.createUser)); // localhost:3000/api/v1/users
router.patch('/id/:id', auth(Role.Admin), updateUserSchema, awaitHandlerFactory(userController.updateUser)); // localhost:3000/api/v1/users/id/1 , using patch for partial update
router.delete('/id/:id', auth(Role.Admin), awaitHandlerFactory(userController.deleteUser)); // localhost:3000/api/v1/users/id/1


router.post('/login', validateLogin, awaitHandlerFactory(userController.userLogin)); // localhost:3000/api/v1/users/login

module.exports = router;


Enter fullscreen mode Exit fullscreen mode

上面的例子展示了如何定义路由。让我们试着把它分解成几个部分:

  • 您可以使用 express.Router() 创建路由器。每个路由可以加载一个处理业务逻辑的中间件函数。例如,UserController 就包含所有主要的中间件。要使用路由器,应将其导出为模块,并在主应用中使用 app.use(router_module) 进行调用。
  • 我们使用 auth 中间件进行用户身份验证和授权,用于检查路由的用户令牌或用户角色。在我们的示例中,一些路由使用 auth 中间件来检查用户身份验证和授权。此中间件将在主中间件(包含业务逻辑的中间件)之前触发。必须调用下一个回调才能将控制权传递给下一个中间件方法。否则,请求将被挂起。
  • awaitHandlerFactory(try-catch 中间件)用于包装所有异步中间件。这样,如果其中一个中间件抛出错误,awaitHandlerFactory 就会捕获该错误。您可以看到,我们所有的中间件函数都用 awaitHandlerFactory 中间件包装,这有助于我们通过在一个地方使用 try-catch 来处理错误。
  • 此外,在启动下一个中间件之前,我们有 createUserSchema、updateUserSchema 和 validateLogin 模式来验证主体。

HTTP 方法的语法是:
HTTP 方法语法

创建控制器:

在 controllers 目录中创建 user.controller.js 文件并复制粘贴以下内容:



const UserModel = require('../models/user.model');
const HttpException = require('../utils/HttpException.utils');
const { validationResult } = require('express-validator');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const dotenv = require('dotenv');
dotenv.config();

/******************************************************************************
 *                              User Controller
 ******************************************************************************/
class UserController {
    getAllUsers = async (req, res, next) => {
        let userList = await UserModel.find();
        if (!userList.length) {
            throw new HttpException(404, 'Users not found');
        }

        userList = userList.map(user => {
            const { password, ...userWithoutPassword } = user;
            return userWithoutPassword;
        });

        res.send(userList);
    };

    getUserById = async (req, res, next) => {
        const user = await UserModel.findOne({ id: req.params.id });
        if (!user) {
            throw new HttpException(404, 'User not found');
        }

        const { password, ...userWithoutPassword } = user;

        res.send(userWithoutPassword);
    };

    getUserByuserName = async (req, res, next) => {
        const user = await UserModel.findOne({ username: req.params.username });
        if (!user) {
            throw new HttpException(404, 'User not found');
        }

        const { password, ...userWithoutPassword } = user;

        res.send(userWithoutPassword);
    };

    getCurrentUser = async (req, res, next) => {
        const { password, ...userWithoutPassword } = req.currentUser;

        res.send(userWithoutPassword);
    };

    createUser = async (req, res, next) => {
        this.checkValidation(req);

        await this.hashPassword(req);

        const result = await UserModel.create(req.body);

        if (!result) {
            throw new HttpException(500, 'Something went wrong');
        }

        res.status(201).send('User was created!');
    };

    updateUser = async (req, res, next) => {
        this.checkValidation(req);

        await this.hashPassword(req);

        const { confirm_password, ...restOfUpdates } = req.body;

        // do the update query and get the result
        // it can be partial edit
        const result = await UserModel.update(restOfUpdates, req.params.id);

        if (!result) {
            throw new HttpException(404, 'Something went wrong');
        }

        const { affectedRows, changedRows, info } = result;

        const message = !affectedRows ? 'User not found' :
            affectedRows && changedRows ? 'User updated successfully' : 'Updated faild';

        res.send({ message, info });
    };

    deleteUser = async (req, res, next) => {
        const result = await UserModel.delete(req.params.id);
        if (!result) {
            throw new HttpException(404, 'User not found');
        }
        res.send('User has been deleted');
    };

    userLogin = async (req, res, next) => {
        this.checkValidation(req);

        const { email, password: pass } = req.body;

        const user = await UserModel.findOne({ email });

        if (!user) {
            throw new HttpException(401, 'Unable to login!');
        }

        const isMatch = await bcrypt.compare(pass, user.password);

        if (!isMatch) {
            throw new HttpException(401, 'Incorrect password!');
        }

        // user matched!
        const secretKey = process.env.SECRET_JWT || "";
        const token = jwt.sign({ user_id: user.id.toString() }, secretKey, {
            expiresIn: '24h'
        });

        const { password, ...userWithoutPassword } = user;

        res.send({ ...userWithoutPassword, token });
    };

    checkValidation = (req) => {
        const errors = validationResult(req)
        if (!errors.isEmpty()) {
            throw new HttpException(400, 'Validation faild', errors);
        }
    }

    // hash password if it exists
    hashPassword = async (req) => {
        if (req.body.password) {
            req.body.password = await bcrypt.hash(req.body.password, 8);
        }
    }
}



/******************************************************************************
 *                               Export
 ******************************************************************************/
module.exports = new UserController;


Enter fullscreen mode Exit fullscreen mode

如上所述,控制器文件包含处理路由的业务逻辑。
在我们的示例中,某些方法使用 UserModel 类查询数据库以获取数据。
为了在每个中间件中返回数据,我们使用 res.send(result) 向客户端发送响应。

创建模型:

并在模型目录中创建 user.model.js 文件并复制粘贴以下内容:



const query = require('../db/db-connection');
const { multipleColumnSet } = require('../utils/common.utils');
const Role = require('../utils/userRoles.utils');
class UserModel {
    tableName = 'user';

    find = async (params = {}) => {
        let sql = `SELECT * FROM ${this.tableName}`;

        if (!Object.keys(params).length) {
            return await query(sql);
        }

        const { columnSet, values } = multipleColumnSet(params)
        sql += ` WHERE ${columnSet}`;

        return await query(sql, [...values]);
    }

    findOne = async (params) => {
        const { columnSet, values } = multipleColumnSet(params)

        const sql = `SELECT * FROM ${this.tableName}
        WHERE ${columnSet}`;

        const result = await query(sql, [...values]);

        // return back the first row (user)
        return result[0];
    }

    create = async ({ username, password, first_name, last_name, email, role = Role.SuperUser, age = 0 }) => {
        const sql = `INSERT INTO ${this.tableName}
        (username, password, first_name, last_name, email, role, age) VALUES (?,?,?,?,?,?,?)`;

        const result = await query(sql, [username, password, first_name, last_name, email, role, age]);
        const affectedRows = result ? result.affectedRows : 0;

        return affectedRows;
    }

    update = async (params, id) => {
        const { columnSet, values } = multipleColumnSet(params)

        const sql = `UPDATE user SET ${columnSet} WHERE id = ?`;

        const result = await query(sql, [...values, id]);

        return result;
    }

    delete = async (id) => {
        const sql = `DELETE FROM ${this.tableName}
        WHERE id = ?`;
        const result = await query(sql, [id]);
        const affectedRows = result ? result.affectedRows : 0;

        return affectedRows;
    }
}

module.exports = new UserModel;


Enter fullscreen mode Exit fullscreen mode

这个类负责在控制器和数据库之间建立连接。
它包含了从控制器获取参数、执行查询、准备语句、使用 db-connection 类中的查询方法连接数据库、发送包含预处理语句数组的请求以及返回结果的所有方法。
每个函数都会将结果返回给控制器。

.git忽略:

如果您决定将此项目添加到您的 GitHub,请不要忘记创建一个 .gitignore 文件并复制粘贴以下内容:



node_modules
.env


Enter fullscreen mode Exit fullscreen mode

该文件仅告诉 git 应该忽略哪些文件。
你应该避免使用 node_modules 目录,因为它体积庞大,且对仓库来说并非必需。
当有人克隆此仓库时,他们会使用“npm I”命令安装所有依赖项。
忽略 .env 文件是为了向使用你代码的其他开发者隐藏你的私有配置。

总体项目设计

源代码:

该示例的完整源代码可以在Github上找到

文章来源:https://dev.to/juliest88/how-to-build-rest-api-with-nodejs-express-and-mysql-31jk
PREV
🦸 使用 console.trace 增强你的调试技能
NEXT
问:你的期望薪资是多少?答:没有,请给我一个报价!