使

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

2025-05-25

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

在本教程中,我们将使用 TypeScript 和 Docker,通过 Node、Express 和 MongoDB 从头构建一个 Menu Restaurant API 应用。Docker 部分是可选的。

基本上,我们应该能够:

  • 检索所有菜单
  • 检索一个菜单
  • 创建菜单
  • 更新菜单
  • 删除菜单

太好了,让我们开始吧。

设置

要创建一个新的 Nodejs 项目,我们首先在终端上运行此命令。

yarn init
Enter fullscreen mode Exit fullscreen mode

在初始化项目之前,它会询问几个问题。无论如何,您可以通过-y在命令中添加一个标志来绕过这些问题。

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

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

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

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

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

yarn add typescript
Enter fullscreen mode Exit fullscreen mode

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

yarn add express cors mongoose
Enter fullscreen mode Exit fullscreen mode

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

yarn add -D @types/node @types/express @types/mongoose @types/cors
Enter fullscreen mode Exit fullscreen mode

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

yarn add -D concurrently nodemon
Enter fullscreen mode Exit fullscreen mode

我们需要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"
  }
}
Enter fullscreen mode Exit fullscreen mode

项目已准备就绪。我们现在可以开始编码了。:)

构建 API

我们的工作方式如下:

  • 创建菜单类型
  • 创建菜单模型
  • 创建菜单控制器
  • 添加菜单路由
  • 配置app.ts连接到 Mongo Atlas 并启动服务器。

创建菜单类型

我们将编写一个 Menu 接口,它将扩展Document提供的类型mongoose。稍后与 MongoDB 交互时会用到它。

import { Document } from "mongoose";

export interface IMenu extends Document {
  name: string;
  description: string;
  price: number;
}
Enter fullscreen mode Exit fullscreen mode

创建菜单模型

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

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

首先,我们导入RequestResponse键入,express以便明确输入值。下一步,getMenus创建函数以从数据库中获取数据。

  • 它接收reqres参数并返回一个承诺
  • 在之前创建的模型的帮助下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;
  }
};
Enter fullscreen mode Exit fullscreen mode

与 稍有不同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;
  }
};
Enter fullscreen mode Exit fullscreen mode

id该函数将从对象中提取req,然后将其作为参数传递给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;
  }
};
Enter fullscreen mode Exit fullscreen mode

此函数接受一个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;
  }
};
Enter fullscreen mode Exit fullscreen mode

此函数允许我们从数据库中删除菜单。
在这里,我们提取表单id并将req其作为参数传递给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 };
Enter fullscreen mode Exit fullscreen mode

API 路由

我们将创建五条路由,分别用于从数据库中获取、创建、更新和删除菜单。我们将使用之前创建的控制器,并将它们作为参数传递,以便在定义路由时处理请求。

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

创建服务器

首先,让我们添加一些包含 MongoDB 数据库凭据的环境变量。

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

您可以通过在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;
  });
Enter fullscreen mode Exit fullscreen mode

我们首先导入express库来使用use方法来处理菜单路由。

接下来,我们使用 mongoose 包通过将文件中保存的凭据附加到 URL 来连接到 MongoDB nodemon.json

现在,如果与 MongoDB 数据库的连接成功,服务器将启动,否则将抛出错误。

我们现在已经使用 Node、Express、TypeScript 和 MongoDB 构建了 API。

要启动您的项目,请运行yarn start并点击http://localhost:4000

以下是您可以使用 Postman 或 Insomnia 对 API 进行的一些测试。

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

GET http://localhost:4000/menu
Enter fullscreen mode Exit fullscreen mode

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

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

{
    "name": "Hot Dog",
    "description": "A hot dog",
    "price": 10
}
Enter fullscreen mode Exit fullscreen mode

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

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

{
    "price": 5
}
Enter fullscreen mode Exit fullscreen mode

现在让我们将项目docker化。

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

在这里,我们从基于 Alpine 的 Node.js Docker 镜像开始。它是一个专为安全性和资源效率而设计的轻量级 Linux 发行版。

之后,我们执行如下操作:

  • 设置工作变量
  • 复制package.json文件yarn.lock到我们的应用程序路径
  • 安装项目依赖项
  • 最后复制整个项目

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

.dockerignore
Dockerfile
node_modules
Enter fullscreen mode Exit fullscreen mode

完成后,我们现在可以添加 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"
Enter fullscreen mode Exit fullscreen mode

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

docker-compose up -d --build
Enter fullscreen mode Exit fullscreen mode

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

结论

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

每篇文章都可以变得更好,所以欢迎在评论区提出您的建议或问题。😉

在此处查看本教程的代码

文章来源:https://dev.to/koladev/build-an-api-using-node-express-mongodb-and-docker-128p
PREV
使用 Django、Django REST 和 Next.js 构建全栈应用程序
NEXT
在 JavaScript 中编写函数的 5 种方法