使用 Node、 Express、 MongoDB 和 Docker 构建一个 API

1,233 阅读9分钟

cover

在本教程中,我们将使用 TypeScript 和 Docker 从头开始构建一个使用 Node、 Express 和 MongoDB 的 餐厅菜单 API 应用程序。Docker 部分是可选的。

基本上,我们应该可以做到以下几个方面:

  • 获取所有菜单
  • 获取一个菜单
  • 创建一个菜单
  • 更新一个菜单
  • 删除一个菜单

很好,我们开始吧。

设置

为了创建一个新的 node.js 项目,我们首先在终端上运行这个命令。

yarn init

在初始化项目之前,它会问一些问题。不过,您可以通过向命令添加一个 -y 标志来绕过这个命令。

下一步是为我们的项目创建一个结构。

├── dist
├── src
   ├── app.ts
   ├── controllers
   |  └── menus
   |     └── index.ts
   ├── models
   |  └── menu.ts
   ├── routes
   |  └── index.ts
   └── types
      └── menu.ts
├── nodemon.json
├── package.json
├── tsconfig.json

让我快速解释一下这个项目的结构。

  • dist 它将作为最终typescript 代码被编译成普通的 JavaScript 代码的输出文件夹,。
  • src 包含了我们 API 的逻辑。
    • app.ts 是服务器的入口。
    • controllers 将包含一些函数用来处理请求和返回模型中的数据到客户端。
    • models 将包含一些允许对我们数据库进行一些基本操作的对象。
  • routes 用于将请求转发给适当的控制器。
  • types 将包含我们项目中对象用到的一些接口

继续,让我们添加一些配置到 tsconfig.json,这将有助于计算机顺从我们的开发偏好。

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "outDir": "dist/js",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["src/types/*.ts", "node_modules", ".vscode", ".idea"]
}

现在我们可以开始安装依赖来启动我们的项目了。首先,我们要启用TypeScript。

yarn add typescript

让我们也添加一些依赖项来使用 Express 和 MongoDB。

yarn add express cors mongoose

接下来,我们将添加它们的类型作为开发依赖项,这将有助于计算机理解这些依赖包。

yarn add -D @types/node @types/express @types/mongoose @types/cors

让我们添加一些依赖项,以便在修改文件时自动重新加载服务器,并同时启动服务器(我们能够在启动服务的同时做出一些更改)。

yarn add -D concurrently nodemon

我们需要在启动服务器和构建项目时候修改 package.json 文件的脚本

下面展示了你的package.json文件应该是什么样。

{
  "name": "menu-node-api",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "cors": "^2.8.5",
    "express": "^4.17.1",
    "mongoose": "^6.0.11",
    "nodemon": "^2.0.13",
    "typescript": "^4.4.4"
  },
  "scripts": {
    "build": "tsc",
    "start": "concurrently \"tsc -w\" \"nodemon dist/js/app.js\""
  },
  "devDependencies": {
    "@types/cors": "^2.8.12",
    "@types/express": "^4.17.13",
    "@types/mongoose": "^5.11.97",
    "@types/node": "^16.11.1",
    "concurrently": "^6.3.0"
  }
}

项目已经准备就绪。我们现在开始写代码。 :)

构建API

下面列出了我们工作如何展开:

  • 创建一个菜单类型
  • 创建一个菜单模型
  • 创建一个菜单控制器
  • 添加一个菜单路由
  • 配置``app.ts` 连接到Mongo Atlas(一个 MongoDB 数据库即服务平台)并启动服务器。

创建菜单类型

我们要写一个由mongoose提供的Document类型扩展的菜单接口。稍后与 MongoDB 进行交互将非常有用。

import { Document } from "mongoose";

export interface IMenu extends Document {
  name: string;
  description: string;
  price: number;
}

创建菜单模型

import { IMenu } from "../types/menu";
import { model, Schema } from "mongoose";

const menuSchema: Schema = new Schema(
  {
    name: {
      type: String,
      required: true,
    },
    description: {
      type: String,
      required: true,
    },
    price: {
      type: String,
      required: true,
    },
  },
  { timestamps: true }
);

export default model<IMenu>("Menu", menuSchema);

mongoose 提供了有用的工具来创建模型 。注意,在此之前导出一个模型所用到的IMenu类型。

现在已经编写了模型,我们可以开始与数据库的其他文件进行交互了。

创建控制器

下面我们将要写5个控制器。

  • getMenus: 用来从数据库获取所有的菜单对象
  • addMenu: 用来创建一个菜单
  • updateMenu: 用来更新一个菜单
  • deleteMenu: 用来删除一个菜单
  • retrieveMenu: 用来检索一个菜单

让我们从getMenus 开始。

// ./src/controllers/menus/index.ts

import { Response, Request } from "express";
import { IMenu } from "../../types/menu";
import Menu from "../../models/menu";

const getMenus = async (req: Request, res: Response): Promise<void> => {
  try {
    const menus: IMenu[] = await Menu.find();
    res.status(200).json({ menus });
  } catch (error) {
    throw error;
  }
};

首先,我们从express显示导入RequestResponse类型。下一步,下一步,创建 getMenus 函数以从数据库中获取数据。

  • 它接收到一个reqres 参数 并返回一个promise 类型
  • 随着Menu模型早期的创建的帮助,我们立马可以从MongoDB数据库获取所有的menus并返回一个包括这些对象的响应结果。

很好,让我们来看看addMenu 控制器。

const addMenu = async (req: Request, res: Response): Promise<void> => {
  try {
    const body = req.body as Pick<IMenu, "name" | "description" | "price">;
    const menu: IMenu = new Menu({
      name: body.name,
      description: body.description,
      price: body.price,
    });

    const newMenu: IMenu = await menu.save();

    res.status(201).json(newMenu);
  } catch (error) {
    throw error;
  }
};

getMenus 稍有不同的是,这个函数现在接收一个 body 对象,该对象将包含用户输入的数据。

接下来,我们使用类型映射来避免类型,并确保 body 变量匹配 IMenu,然后我们创建一个新的 Menu,然后将 Menu 保存到数据库中。

const retrieveMenu = async (req: Request, res: Response): Promise<void> => {
  try {
    const {
      params: { id },
    } = req;
    const menu: IMenu | null = await Menu.findById({ _id: id });

    res.status(menu ? 200 : 404).json({ menu });
  } catch (error) {
    throw error;
  }
};

该函数将从 req 对象中提取 id,然后将其作为参数传递给 findById 方法以访问该对象并将其返回给客户端。

const updateMenu = async (req: Request, res: Response): Promise<void> => {
  try {
    const {
      params: { id },
      body,
    } = req;

    const updateMenu: IMenu | null = await Menu.findByIdAndUpdate(
      { _id: id },
      body
    );

    res.status(updateMenu ? 200 : 404).json({
      menu: updateMenu,
    });
  } catch (error) {
    throw error;
  }
};

这个函数接受一个id 参数,但也接受body对象。

接下来,我们使用 findByIdAndUpdate 从数据库中查询对应的菜单并对其进行更新。

const deleteMenu = async (req: Request, res: Response): Promise<void> => {
  try {
    const deletedMenu: IMenu | null = await Menu.findByIdAndRemove(
      req.params.id
    );
    res.status(204).json({
      todo: deletedMenu,
    });
  } catch (error) {
    throw error;
  }
};

这个函数允许我们从数据库中删除一个菜单。

在这里,我们从 req 中提取 id 并将其作为参数传递给 findByIdAndRemove 方法,以查询对应的 菜单 并从数据库中删除它。

我们控制器已经准备好了接下来我们导出他们

下面是src/controllers/menus/index.ts的最终代码文件。

import { Response, Request } from "express";
import { IMenu } from "../../types/menu";
import Menu from "../../models/menu";

const getMenus = async (req: Request, res: Response): Promise<void> => {
  try {
    const menus: IMenu[] = await Menu.find();
    res.status(200).json({ menus });
  } catch (error) {
    throw error;
  }
};

const retrieveMenu = async (req: Request, res: Response): Promise<void> => {
  try {
    const {
      params: { id },
    } = req;
    const menu: IMenu | null = await Menu.findById({ _id: id });

    res.status(menu ? 200 : 404).json({ menu });
  } catch (error) {
    throw error;
  }
};

const addMenu = async (req: Request, res: Response): Promise<void> => {
  try {
    const body = req.body as Pick<IMenu, "name" | "description" | "price">;
    const menu: IMenu = new Menu({
      name: body.name,
      description: body.description,
      price: body.price,
    });

    const newMenu: IMenu = await menu.save();

    res.status(201).json(newMenu);
  } catch (error) {
    throw error;
  }
};

const updateMenu = async (req: Request, res: Response): Promise<void> => {
  try {
    const {
      params: { id },
      body,
    } = req;

    const updateMenu: IMenu | null = await Menu.findByIdAndUpdate(
      { _id: id },
      body
    );

    res.status(updateMenu ? 200 : 404).json({
      menu: updateMenu,
    });
  } catch (error) {
    throw error;
  }
};

const deleteMenu = async (req: Request, res: Response): Promise<void> => {
  try {
    const deletedMenu: IMenu | null = await Menu.findByIdAndRemove(
      req.params.id
    );
    res.status(204).json({
      todo: deletedMenu,
    });
  } catch (error) {
    throw error;
  }
};
export { getMenus, addMenu, updateMenu, deleteMenu, retrieveMenu };

API 路由

我们为了从数据库获取,创建,更新,删除菜单要创建5个路由。我们要使用我们已经创建的控制器,并在定义路由时将它们作为参数传递以处理请求。

import { Router } from "express";
import {
  getMenus,
  addMenu,
  updateMenu,
  deleteMenu,
  retrieveMenu,
} from "../controllers/menus";

const menuRoutes: Router = Router();

menuRoutes.get("/menu", getMenus);
menuRoutes.post("/menu", addMenu);
menuRoutes.put("/menu/:id", updateMenu);
menuRoutes.delete("/menu/:id", deleteMenu);
menuRoutes.get("/menu/:id", retrieveMenu);

export default menuRoutes;

创建服务器

首先,让我们添加一些 env 变量,它们将包含 MongoDB 数据库的凭据

// .nodemon.js
{
    "env": {
        "MONGO_USER": "your-username",
        "MONGO_PASSWORD": "your-password",
        "MONGO_DB": "your-db-name"
    }
}

您可以通过在 MongoDB Atlas. 上创建一个新的集群来获得凭据。

由于这些是数据库凭据,所以请务必不要将凭据推送到存储库上或公开它们。

// .src/app.ts
import express from "express";
import mongoose from "mongoose";
import cors from "cors";
import menuRoutes from "./routes";

const app = express();

const PORT: string | number = process.env.PORT || 4000;

app.use(cors());
app.use(express.json());
app.use(menuRoutes);

const uri: string = `mongodb+srv://${process.env.MONGO_USER}:${process.env.MONGO_PASSWORD}@cluster0.raz9g.mongodb.net/${process.env.MONGO_DB}?retryWrites=true&w=majority`
mongoose
  .connect(uri)
  .then(() =>
    app.listen(PORT, () =>
      console.log(`Server running on http://localhost:${PORT}`)
    )
  )
  .catch((error) => {
    throw error;
  });

我们首先通过导入express库的use方法来处理菜单的路由。

下一步,我们使用mongoose 包连接MongoDB 通过在URL后面追加上 nodemon.json 文件中保存的凭据信息。

现在,如果连接MongoDB 成功了服务器将启动,否则将会抛出一个错误。

我们现在已经使用 Node, Express, TypeScript, and MongoDB.完成了搭建。

要启动项目,运行yarn start并访问http://localhost:4000

下面你可以使用Postman 或者Insomnia对API进行一些测试。

获取所有菜单 - http://localhost:4000/menu

GET http://localhost:4000/menu

创建一个菜单 - http://localhost:4000/menu

POST http://localhost:4000/menu
Content-Type: application/json

{
    "name": "Hot Dog",
    "description": "A hot dog",
    "price": 10
}

更新一个菜单 - http://localhost:4000/menu/<menuId>

PUT http://localhost:4000/menu/<menuId>
Content-Type: application/json

{
    "price": 5
}

我们接下来使用容器部署项目

Docker + Docker Compose (可选)

Docker 是一个开放的平台,用于在容器内开发、运行和运行应用程序。

为什么使用 Docker?

它可以帮助您将应用程序与基础环境分离开来,并帮助您更快地交付代码。

如果这是您第一次使用 Docker,我强烈建议您浏览一个快速教程并阅读一些相关文档。

以下是一些对我有帮助的资源:

Dockerfile

Dockerfile 表示一个文本文档,其中包含可以在命令行上调用以创建镜像的所有命令。

添加一个Dockerfile 到项目根路径:

FROM node:16-alpine

WORKDIR /app

COPY package.json ./

COPY yarn.lock ./

RUN yarn install --frozen-lockfile

COPY . .

在这里,我们基于Alpine-based Docker Image for Node 开始。这是一个为安全和资源效率而设计的轻量级Linux发行版。

在此之后,我们执行以下操作:

  • 设置工作变量
  • 拷贝package.jsonyarn.lock文件到我们的应用路径
  • 安装项目依赖
  • 最后拷贝整个项目

另外,我们还要添加一个.dockerignore 文件。

.dockerignore
Dockerfile
node_modules

完成之后,我们现在可以添加 docker-compose。

Docker Compose是一个很好的工具(< 3)。你可以用它来定义和运行多容器 Docker 应用程序。

我们需要做什么?只需要一个 YAML 文件,其中包含我们应用服务的所有配置。

然后,使用 docker-compose 命令,我们可以创建并启动所有这些服务

version: '3.8'
services:
  api:
    container_name: node_api
    restart: on-failure
    build: .
    volumes:
      - ./src:/app/src
    ports:
      - "4000:4000"
    command: >
      sh -c "yarn start"

设置已经完成。让我们构建容器并测试在本地工作是否一切正常。

docker-compose up -d --build

你的项目将在 https://localhost:4000/上运行。

总结

在本文中,我们学习了如何使用NodeJS、TypeScript、 Express、 MongoDB 和 Docker 构建 API。

为了让每一篇文章都可以写得更好,欢迎在评论区提出你的建议或问题。😉

点击这里查看本教程的代码 here.