使用MySQL构建NodeJS、TypeScript Rest API的分布指南

2021年11月28日06:38:45 发表评论 930 次浏览

TypeScript如何构建Rest API?MySQL 无疑是每个 Node 开发人员技术堆栈中关系数据库的首选之一。Node 易于创建后端 API,再加上 MySQL 支持复杂查询操作的能力,为开发人员构建高级 Web 后端提供了一种简单的方法。

在本TypeScript构建Rest API教程中,我们将使用 Express 框架为在线商店开发一个简单的 REST API。MySQL 是我们选择的数据库。我们决定使用 Typescript 构建这个 API,而不是使用简单的 Javascript 来实现。

Typescript 类型支持为开发人员滥用类型留下了很小的空间。它帮助我们编写更干净、可重用的代码。如果你是 Typescript 的初学者或想要刷新你对该语言的记忆,请阅读我们的 Javascript 开发人员 Typescript指南, 然后再进行下一步。

随着最初的介绍,让我们现在开始。


在我们开始之前……

如何构建TypeScript Rest API?在我们开始本教程之前,请确保你拥有我们需要设置的所有工具。假设你已经安装了 Node.js,请在你的设备上安装 MySQL, 然后再继续。

设置数据库

正如我之前提到的,我们正在为一个简单的在线商店创建一个 API,该商店在其数据库中存储产品和注册客户列表。当客户下订单时,他们的详细信息也存储在此数据库中。

我们的数据库模式总共有 3 个表:Product、Customer 和 ProductOrder。

数据模型
数据模型

我将使用常规 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 项目环境

如何构建TypeScript Rest API?像往常一样,我们使用该npm init命令来初始化我们的 Node.js 项目,作为设置的第一步。

接下来,我们必须安装我们将在这个项目中使用的 npm 包。有不少。我们将首先安装项目依赖项。

npm install express body-parser mysql2 dotenv

在这里,我们用于dotenv将环境变量导入到项目中并mysql2管理数据库连接。

然后,安装 Typescript 作为开发依赖项。

npm install typescript --save-dev

我们还必须为我们在项目中使用的包安装 Typescript 类型定义。由于这些包中的大多数没有类型定义,我们使用 @types npm 命名空间,其中相关类型定义托管在绝对类型 项目中。

npm install @types/node @types/express @types/body-parser @types/mysql @types/dotenv --save-dev

接下来,我们应该将我们的项目初始化为 Typescript 项目。为此,请运行以下命令。

npx tsc --init

这会将tsconfig.json文件添加到你的项目中。我们使用它来配置与项目相关的 Typescript 选项。

TypeScript构建Rest API教程:当你打开tsconfig.json文件时,你会看到一堆注释代码。对于我们的项目,我们需要取消对以下选项的注释并将它们设置为如下所示的值。

"compilerOptions": {
    "target": "es6",   
    "module": "commonjs",
    "outDir": "dist",
    "strict": true,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
}

这些选项将在 Typescript 编译成 Javascript 时考虑。在outDir我们在这里提供的是已编译.js的文件将被存储。

作为最后一步,我们必须在启动 Node 应用程序之前修改 package.json 文件中的启动脚本以编译 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"

可以看到,大部分环境变量都与我们之前创建的数据库相关。


如何构建TypeScript Rest API:为 API 定义新类型

我们必须为 Product、Customer 和 Order 对象定义新的 Typescript 类型。我们将所有类型文件存储在 types 目录中。

//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。有时我们想要处理详细的产品对象。出于这个原因,我们使用两种产品类型,一种扩展了另一种。在定义 Customer 和 Order 类型时,你会看到类似的行为。

//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
});

我们导出建立的连接对象,方便分别定义不同类型的数据库操作。


TypeScript构建Rest API教程:定义数据库操作

接下来,让我们为数据库创建用于 create、findOne、findAll 和 update 操作的函数。我只执行与订单类型相关的操作。但是你也可以为其他数据类型实现这些操作。

请注意,我们将为此任务编写简单的 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 官方文档 以了解我们在本教程中使用的基本插入、选择、更新语句。

TypeScript如何构建Rest API?接下来,让我们实现 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);
  });
}

在这里,我们也遵循与 create 函数类似的过程。在 SQL 语句中,我们必须加入 ProductRecord、Customer、Product 表来检索客户和订单中包含的产品的完整记录。我们使用 OrderProduct 表中定义的外键进行连接。

检索数据后,我们必须创建一个 Order 类型的对象。由于我们正在检索产品和客户对象的详细信息,因此我们从之前定义的 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);
    }
  );
}

至此,我们完成了订单相关的数据库操作的功能。


实现路由处理程序

如何构建TypeScript Rest API?下一步,我们将实现/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 路由器来定义路由,因此我们可以在以下实现中使用相对于 /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 中

TypeScript如何构建Rest API?我们现在已完成添加 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。


TypeScript构建Rest API教程概括

如何构建TypeScript Rest API?在本教程中,我们学习了如何使用 Node.js 和 MySQL 创建具有 Typescript 类型支持的 REST API。这 3 项技术完美结合,可以快速轻松地创建 API。因此,我希望本教程能够证明对你将来编写良好的后端 API 是无价的。要获得有关该主题的更多经验,你可以尝试实施我在本教程中提出的建议作为下一步。

木子山

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: