- 原文地址:Build an API using Node, Express, MongoDB, and Docker
- 原文作者:Mangabo Kolawole
- 译文出自:掘金翻译计划
在本教程中,我们将使用 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
显示导入Request
和Response
类型。下一步,下一步,创建 getMenus
函数以从数据库中获取数据。
- 它接收到一个
req
和res
参数 并返回一个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.json
和yarn.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.