使用MySQL构建NodeJS、TypeScript Rest API的指南
在每个Node开发者的技术栈中,MySQL无疑是关系型数据库的最佳选择之一。Node创建后台API的便利性与MySQL支持复杂查询操作的能力相结合,为开发者构建高级网络后台提供了一种简单的方法。
在本教程中,我们将用Express框架为一个在线商店开发一个简单的REST API。MySQL是我们选择的数据库。我们没有使用简单的Javascript来实现,而是决定使用Typescript来构建这个API。
Typescript的类型支持几乎没有给开发者留下滥用类型的空间。它帮助我们写出更干净、可重用的代码。如果你是Typescript的初学者,或者想加深对该语言的记忆,请在进行下一步之前阅读我们的Javascript开发者Typescript指南。
随着初步介绍的结束,我们现在开始吧。
在我们开始之前...
在我们开始教程之前,请确保你已经设置好了所有我们需要的工具。假设你已经安装了Node.js,在继续之前在你的设备上安装MySQL。
设置数据库
正如我之前提到的,我们正在为一个简单的在线商店创建一个API,该商店在其数据库中存储了一个产品和注册客户的列表。当客户下产品订单时,他们的详细信息也会存储在这个数据库中。
总的来说,我们的数据库模式有3个表。产品,客户,和产品订单。
我将使用常规的SQL查询来创建它们。如果你愿意,你可以继续使用一个工具GUI工具来创建数据库模式。
确保你的MySQL服务器正在运行,然后在命令行上运行这个命令。(你必须把MySQL添加到环境变量中,才能直接使用mysql命令)。
mysql -u <username> -p <password>
它将带你到MySQL shell,你可以直接在数据库上运行SQL查询。
现在,我们可以为我们的项目创建一个新的数据库。
create database OnlineStore
使用下面的命令来切换到新创建的数据库。
use OnlineStore;
然后,运行下面的查询来创建我们需要的表。
CREATE TABLE Product (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100),
description VARCHAR(255),
instock_quantity INT,
price DECIMAL(8, 2)
);
CREATE TABLE Customer (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50),
password VARCHAR(255),
email VARCHAR(255) UNIQUE
);
CREATE TABLE ProductOrder (
order_id INT AUTO_INCREMENT PRIMARY KEY,
product_id INT,
customer_id INT,
product_quantity INT,
FOREIGN KEY (product_id) REFERENCES Product(id),
FOREIGN KEY (customer_id) REFERENCES Customer(id)
);
使用与下面类似的查询,将一些数据输入到创建的表中。
INSERT INTO Product VALUES (1, "Apple MacBook Pro", "15 inch, i7, 16GB RAM", 5, 667.00);
INSERT INTO Customer VALUES (1, "Anjalee", "2w33he94yg4mx88j9j2hy4uhd32w", "anjalee@gmail.com");
INSERT INTO ProductOrder VALUES (1, 1, 1, 1);
很好!我们的数据库模式现在已经完成。我们的数据库模式现在已经完成。我们可以前往Node.js,在下一步开始实现API。
设置Node.js项目环境
像往常一样,我们使用npm init
命令来初始化我们的Node.js项目,作为设置的第一步。
接下来,我们必须安装我们将在这个项目中使用的npm包。这里有相当多的包。我们将首先安装项目的依赖性。
npm install express body-parser mysql2 dotenv
在这里,我们用dotenv
,向项目导入环境变量,用mysql2
,管理数据库连接。
然后,安装Typescript作为开发依赖。
npm install typescript --save-dev
我们还必须为我们在项目中使用的包安装Typescript类型定义。由于这些包大多没有类型定义,我们使用@types npm命名空间,相关的类型定义被托管在Definitely Typed项目中。
npm install @types/node @types/express @types/body-parser @types/mysql @types/dotenv --save-dev
接下来,我们应该把我们的项目初始化为一个Typescript项目。为此,运行以下命令。
npx tsc --init
这将把tsconfig.json
文件添加到你的项目中。我们用它来配置与项目有关的Typescript选项。
当你打开tsconfig.json
文件时,你会看到一堆有注释的代码。对于我们的项目,我们需要取消对以下选项的注释,并将它们设置为如下所示的值。
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
}
这些选项将在Typescript被编译成Javascript时被考虑。我们在这里提供的outDir
,就是要将编译后的.js
文件存放在这里。
作为最后一步,我们必须修改package.json文件中的启动脚本,以便在启动Node应用之前编译Typescript。
"scripts": {
"start": "tsc && node dist/app.js",
},
dist/app.js
文件是我们用来编写代码的app.ts
文件的编译版本。
项目的目录结构
我们的项目有一个类似于下图的简单目录结构。
|-.env
|-package.json
|-tsconfig.json
|-dist/
|-app.ts
|-db.ts
|-models/
|-routes/
|-types/
创建.env文件
我们使用.env文件来存储应用程序的环境变量。
PORT=3000
DB_HOST="localhost"
DB_USER="username"
DB_PWD="****"
DB_NAME="OnlineStore"
正如你所看到的,大部分的环境变量都与我们之前创建的数据库有关。
为API定义新的类型
我们必须为产品、客户和订单对象定义新的Typescript类型。我们将所有的类型文件存储在type目录下。
//file types/customer.ts
export interface BasicProduct {
id: number,
}
export interface Product extends BasicProduct {
name: string,
description: string,
instockQuantity: number,
price: number
}
在这里,我们已经创建了两个产品类型。第一个类型,BasicProduct,在其字段中只包含产品ID。第二个类型,Product,扩展了第一个接口,并创建了一个有详细细节的类型。
在我们的应用程序中,有时我们只想处理一个产品的ID。另外一些时候,我们想处理一个详细的产品对象。出于这个原因,我们使用了两个产品类型,一个扩展了另一个。在定义客户和订单类型时,你会看到类似的行为。
//file types/customer.ts
export interface BasicCustomer {
id: number,
}
export interface Customer extends BasicCustomer{
name: string,
email?: string,
password?: string
}
在定义订单类型时,我们可以使用先前创建的客户和产品类型分别作为客户和产品字段的类型。在我们定义的三个订单类型中,相关类型被用于客户和产品字段。
//file types/order.ts
import {BasicProduct, Product} from "./product";
import {BasicCustomer, Customer} from "./customer";
export interface BasicOrder {
product: BasicProduct,
customer: BasicCustomer,
productQuantity: number
}
export interface Order extends BasicOrder {
orderId: number
}
export interface OrderWithDetails extends Order{
product: Product,
customer: Customer,
}
在第一次创建订单时,定义了没有id的BasicOrder类型很有用(因为新订单还没有ID)。
连接到数据库
在mysql2包的帮助下,连接到我们之前创建的数据库是一个简单的步骤。
import mysql from "mysql2";
import * as dotenv from "dotenv";
dotenv.config();
export const db = mysql.createConnection({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PWD,
database: process.env.DB_NAME
});
我们导出已建立的连接对象,以便于分别定义不同类型的数据库操作。
定义数据库操作
接下来,让我们为数据库的创建、findOne、findAll和更新操作创建函数。我只实现与订单类型相关的操作。但你也可以为其他数据类型实现这些操作。
请注意,我们将为这项任务编写普通的SQL语句。如果你想使用ORM而不是手动编写语句,Node提供了几种与Typescript兼容的ORM,如TypeORM和Sequelize。
First, import the objects we will need for the implementation.
//file models/order.ts
import {BasicOrder, Order, OrderWithDetails} from "../types/order";
import {db} from "../db";
import { OkPacket, RowDataPacket } from "mysql2";
接下来,我们来实现创建函数。它被用来在ProductRecord表中插入一条新的订单记录。
export const create = (order: BasicOrder, callback: Function) => {
const queryString = "INSERT INTO ProductOrder (product_id, customer_id, product_quantity) VALUES (?, ?, ?)"
db.query(
queryString,
[order.product.id, order.customer.id, order.productQuantity],
(err, result) => {
if (err) {callback(err)};
const insertId = (<OkPacket> result).insertId;
callback(null, insertId);
}
);
};
我们使用导入的db对象来查询数据库并通过回调返回订单记录的insertId。由于返回的结果是几种类型的联合,我们做了一个简单的转换,将其转换为OkPacket类型,即插入操作返回的类型。
当向查询函数传递SQL语句时,一定要注意不要在字符串中直接使用用户提供的输入。这是一种使你的系统容易受到SQL注入攻击的做法。相反,在应该添加变量的地方使用? 符号,并将变量作为数组传递给查询函数。
如果你不熟悉SQL语句,请参考MySQL官方文档,了解我们在本教程中使用的基本插入、选择、更新语句。
接下来,让我们实现findOne函数,它根据订单ID从ProductOrder表中选择一条记录。
export const findOne = (orderId: number, callback: Function) => {
const queryString = `
SELECT
o.*,
p.*,
c.name AS customer_name,
c.email
FROM ProductOrder AS o
INNER JOIN Customer AS c ON c.id=o.customer_id
INNER JOIN Product AS p ON p.id=o.product_id
WHERE o.order_id=?`
db.query(queryString, orderId, (err, result) => {
if (err) {callback(err)}
const row = (<RowDataPacket> result)[0];
const order: OrderWithDetails = {
orderId: row.order_id,
customer: {
id: row.cusomer_id,
name: row.customer_name,
email: row.email
},
product: {
id: row.product_id,
name: row.name,
description: row.description,
instockQuantity: row.instock_quantity,
price: row.price
},
productQuantity: row.product_quantity
}
callback(null, order);
});
}
在这里,我们也遵循类似于创建函数的过程。在SQL语句中,我们必须连接ProductRecord、Customer、Product表以检索订单中包含的客户和产品的完整记录。我们使用在OrderProduct表中定义的外键来进行连接。
检索完数据后,我们必须创建一个订单类型的对象。由于我们要检索产品和客户对象的详细信息,我们从之前定义的3种订单类型中选择OrderWithDetails作为我们的类型。
现在我们可以按照同样的模式实现另外两个数据库操作,findAll和update。
export const findAll = (callback: Function) => {
const queryString = `
SELECT
o.*,
p.*,
c.name AS customer_name,
c.email
FROM ProductOrder AS o
INNER JOIN Customer AS c ON c.id=o.customer_id
INNER JOIN Product AS p ON p.id=o.product_id`
db.query(queryString, (err, result) => {
if (err) {callback(err)}
const rows = <RowDataPacket[]> result;
const orders: Order[] = [];
rows.forEach(row => {
const order: OrderWithDetails = {
orderId: row.order_id,
customer: {
id: row.customer_id,
name: row.customer_name,
email: row.email
},
product: {
id: row.product_id,
name: row.name,
description: row.description,
instockQuantity: row.instock_quantity,
price: row.price
},
productQuantity: row.product_quantity
}
orders.push(order);
});
callback(null, orders);
});
}
export const update = (order: Order, callback: Function) => {
const queryString = `UPDATE ProductOrder SET product_id=?, product_quantity=? WHERE order_id=?`;
db.query(
queryString,
[order.product.id, order.productQuantity, order.orderId],
(err, result) => {
if (err) {callback(err)}
callback(null);
}
);
}
这样,我们就完成了与订单相关的数据库操作的功能。
实现路由处理程序
下一步,我们将实现/orders
端点的路由处理程序。你可以按照我们在这里使用的模式来实现/customer
和/product
端点。
对于我们的REST API,我们将定义4个端点来从客户端发送请求。
//get all order objects
GET orders/
//create a new order
POST orders/
//get order by order ID
GET orders/:id
//update the order given by order ID
PUT orders/:id
由于我们使用Express Router来定义路由,我们可以在下面的实现中使用与/orders路由相对的路径。
由于我们在数据库模型中实现了数据检索逻辑,在路由处理程序中,我们只需要使用相关函数获取这些数据,并将它们传递给客户端。
让我们把路由处理逻辑添加到orderRouter.ts文件中。
import express, {Request, Response} from "express";
import * as orderModel from "../models/order";
import {Order, BasicOrder} from "../types/order";
const orderRouter = express.Router();
orderRouter.get("/", async (req: Request, res: Response) => {
orderModel.findAll((err: Error, orders: Order[]) => {
if (err) {
return res.status(500).json({"errorMessage": err.message});
}
res.status(200).json({"data": orders});
});
});
orderRouter.post("/", async (req: Request, res: Response) => {
const newOrder: BasicOrder = req.body;
orderModel.create(newOrder, (err: Error, orderId: number) => {
if (err) {
return res.status(500).json({"message": err.message});
}
res.status(200).json({"orderId": orderId});
});
});
orderRouter.get("/:id", async (req: Request, res: Response) => {
const orderId: number = Number(req.params.id);
orderModel.findOne(orderId, (err: Error, order: Order) => {
if (err) {
return res.status(500).json({"message": err.message});
}
res.status(200).json({"data": order});
})
});
orderRouter.put("/:id", async (req: Request, res: Response) => {
const order: Order = req.body;
orderModel.update(order, (err: Error) => {
if (err) {
return res.status(500).json({"message": err.message});
}
res.status(200).send();
})
});
export {orderRouter};
把所有东西放在app.ts中
我们现在已经完成了添加我们API的内部逻辑。唯一要做的是把所有东西放在app.ts文件中,这是我们API的入口,并创建监听和响应请求的服务器。
import * as dotenv from "dotenv";
import express from "express";
import * as bodyParser from "body-parser";
import {orderRouter} from "./routes/orderRouter";
const app = express();
dotenv.config();
app.use(bodyParser.json());
app.use("/orders", orderRouter);
app.listen(process.env.PORT, () => {
console.log("Node server started running");
});
这就是了!我们在短时间内创建了带有MYSQL和Typescript的简单Node.js REST API。
总结
在本教程中,我们学习了如何利用Node.js和MySQL以及Typescript的类型支持创建一个REST API。这3种技术是快速、轻松创建API的完美组合。所以,我希望这个教程能证明对你将来写好后端API是非常有价值的。为了获得更多这方面的经验,你可以尝试实施我在本教程中提出的建议,作为你的下一步。