介绍 (Introduction)
In this article we will discuss what an API is, what it means to be RESTful, and how to implement these using Node.js. The Node.js packages we will be using will be Express for our API endpoints and Sequelize to query our database.
在本文中,我们将讨论API是什么,RESTful的含义以及如何使用Node.js来实现这些API。 我们将使用的Node.js包将Express用作API端点,并使用Sequelize来查询数据库。
Learning how to create an API is a delicate process. Developers just want to build endpoints fast, so they can quickly get something ready for a webpage to consume. However, learning how to make things RESTful will make your API more consistent, predictable, and scalable.
学习如何创建API是一个微妙的过程。 开发人员只想快速构建端点,以便他们可以快速准备好要使用的网页。 但是,学习如何使事物成为RESTful将使您的API更加一致,可预测和可扩展。
This article is assuming you know how to create an Express server and connect to a database with Sequelize.
本文假设您知道如何创建Express服务器并使用Sequelize连接到数据库。
A full code example is available if you just want to get into the code.
如果您只想进入代码,则可以使用完整的代码示例。
术语 (Terminology)
Let’s define what it means to build a RESTful API.
让我们定义构建RESTful API的含义。
Creating an API (Application Programming Interface) is how we can setup logical actions to perform certain functions in our application. For example, we can setup a function called Create Customer and that will provide us an interface, so we don’t have to understand how it will actually create the customer, all we need to know is how to call the function. It’s then up to the programmer to implement the requirements for the API.
创建API(A pplication P AGC软件我覆盖整个院落)是我们如何建立合理的行动,在我们的应用程序执行某些功能。 例如,我们可以设置一个名为Create Customer的函数,该函数将为我们提供一个接口,因此我们不必了解它将如何实际创建客户,我们只需要知道如何调用该函数即可。 然后由程序员来实现API的要求。
Creating a RESTful (REpresentation State Transfer) API means we will follow a pattern of how to logically setup endpoints by using HTTP methods. For example, when you type in your browser to go to a webpage it will call the HTTP GET method to retrieve the webpage for display. We will discuss later all the methods we need to know to create our RESTful API.
创建一个RESTful(RE 的 Presentations泰特贸易交接)API的手段,我们将遵循的模式如何建立逻辑端点通过使用HTTP方法。 例如,当您键入浏览器以访问网页时,它将调用HTTP GET方法来检索要显示的网页。 稍后我们将讨论创建RESTful API所需的所有方法。
入门 (Getting Started)
Before we start, let’s plan out what is required in order to build our API. For example, let’s say we have a React application that has a customer page. On this page the user can create, show all, update, and delete customers. The React application will then make API calls to our Express server, and in turn the API will perform the action on our database. We will also need to prefix our API endpoints with a version number (This is useful if you need to build more APIs but need to maintain older versions).
在开始之前,让我们计划一下构建API所需的条件。 例如,假设我们有一个具有客户页面的React应用程序。 用户可以在此页面上创建,显示全部,更新和删除客户。 然后,React应用程序将对我们的Express服务器进行API调用,然后API将对我们的数据库执行操作。 我们还需要在API端点前面加上版本号(如果您需要构建更多的API但需要维护较旧的版本,这将非常有用)。
RESTful端点 (RESTful Endpoints)
GET / api / v1 / customers (GET /api/v1/customers)
This will return an array of customers in our database.
这将在我们的数据库中返回一系列客户。
GET / api / v1 / customers /:id (GET /api/v1/customers/:id)
This will return one customer specified by the :id parameter.
这将返回一个由:id参数指定的客户。
POST / api / v1 / customers (POST /api/v1/customers)
This will create a customer in our database.
这将在我们的数据库中创建一个客户。
放置/ api / v1 / customers /:id (PUT /api/v1/customers/:id)
This will update a customer specified by the :id parameter.
这将更新由:id参数指定的客户。
删除/ api / v1 / customers /:id (DELETE /api/v1/customers/:id)
This will delete a customer specified by the :id parameter.
这将删除由: id参数指定的客户。
The above link goes over what each HTTP method is and what its intention is. Let’s quickly go over what HTTP methods we will be using.
上面的链接介绍了每种HTTP方法的含义及其目的。 让我们快速浏览一下我们将使用的HTTP方法。
GET: This HTTP method will return a resource located on a server.
GET:此HTTP方法将返回服务器上的资源。
POST: This HTTP method will create a resource.
POST:此HTTP方法将创建资源。
PUT: This HTTP method will update a resource.
PUT:此HTTP方法将更新资源。
DELETE: This HTTP method will delete a resource.
删除:此HTTP方法将删除资源。
Following the HTTP methods and their purpose, means we can start building a RESTful API that is predictable. However we don’t want to write brand new routers every time we create a new database resource. Let’s write some code that will do all the heavy lifting for us.
遵循HTTP方法及其目的,意味着我们可以开始构建可预测的RESTful API。 但是,我们不想每次创建新的数据库资源时都编写全新的路由器。 让我们写一些代码来为我们做所有繁重的工作。
快速路由器 (Express Router)
Creating an Express router is straightforward and unopinionated, so we want to keep that flexibility, but at the same we don’t want to keep writing the same type of router over and over again.
创建Express路由器非常简单明了,因此我们希望保持这种灵活性,但是同时,我们也不想一遍又一遍地编写相同类型的路由器。
So let’s look at a router we can write to create this RESTful API for our customers.
因此,让我们看一下我们可以编写的路由器,以为我们的客户创建此RESTful API。
import express, { Request, Response } from 'express';
import db from '../../db';
const model = db.Customers;
const router = express.Router();const getCustomers = async (req: Request, res: Response) => {
const results = await model.findAll();
res.json(results);
};const getCustomer = async (req: Request, res: Response) => {
const results = await model.findByPk(req.params.id);
res.json(results);
};const createCustomer = async (req: Request, res: Response) => {
const results = await model.create(req.body);
res.json(results);
};const updateCustomer = async (req: Request, res: Response) => {
const results = await model.update(req.body, {
where: {
id: req.params.id,
},
});
res.json(results);
};const deleteCustomer = async (req: Request, res: Response) => {
const results = await model.destroy({
where: {
id: req.params.id,
},
});
res.json(results);
};router.get('/', getCustomers);
router.get(`/:id`, getCustomer);
router.post('/', createCustomer);
router.put(`/:id`, updateCustomer);
router.delete(`/:id`, deleteCustomer);export default router;
There is nothing wrong with this approach, you can have full flexibility if you need to do extra things like send emails, or make other api calls. However, you can see from this example, that it is pretty much a boilerplate router that you can copy and paste into another router and just change the name of customer to whatever the name of the other resource is.
这种方法没有错,如果您需要做其他事情,例如发送电子邮件或进行其他api调用,则可以完全灵活。 但是,从此示例中可以看到,它几乎是一个样板路由器,您可以将其复制并粘贴到另一个路由器中,只需将客户名称更改为其他资源的名称即可。
So let’s make a middleware that can do all this heavy lifting for us.
因此,让我们制作一个可以为我们完成所有繁重工作的中间件。
First let’s create some helper middleware to help with async /await functions and returning our calls in a JSON format.
首先,我们创建一些辅助中间件来帮助异步/ await函数并以JSON格式返回调用。
辅助中间件 (Helper Middleware)
const asyncEndpoint = (endpoint) => {
return async (req: Request, res: Response, next: NextFunction) =>
{
try {
await endpoint(req, res, next);
} catch (e) {
next(e);
}
};
};
This will enable a route to be wrapped in an async / await function and if any error happens we can catch it and send it through to the next Express function.
这将使路由被包装在async / await函数中,如果发生任何错误,我们可以捕获它并将其发送到下一个Express函数。
const toJson = (req: Request, res: Response) => {
res.status(200).json(req.results);
};
This will be placed at the end of each route and if the route is successful it will return the response as JSON.
它将放置在每个路由的末尾,如果路由成功,它将以JSON格式返回响应。
import asyncEndpoint from './asyncEndpoint';export const validateSchema = (...schemas) => {
return asyncEndpoint(async (req, res, next) => {
for (let schemaItem of schemas) {
const { schema, path } = schemaItem;
let validation = schema.validate(req[path], {
abortEarly: false,
});
if (validation.error) {
let messages = validation.error.details.map((i) => i.message);
let errMessage = `Validation errors: ${messages.join(', ')}`;
throw {
status: 400,
message: errMessage,
};
}
}
next();
});
};
This will be our validation middleware that will use the hapi / joi package.
这将是我们的验证中间件,它将使用hapi / joi包。
export const withSequelize = (
req: Request,
res: Response,
next: NextFunction
) => {
const db = req.app.get('db');
const { Sequelize } = db;
const {
page = 0,
limit = 100,
order = '',
attributes = ['id'],
include,
}: any = req.query;
let options: SequelizeOptions = {
offset: page === 0 ? 0 : parseInt(page) * parseInt(limit),
limit: parseInt(limit),
};
let conditions = {}; if (order && isString(order)) {
const [column, direction = 'ASC'] = order.split(',');
options.order = [[Sequelize.col(column), direction]];
} else if (order && isArray(order)) {
options.order = order.map((orderGroup = '') => {
const [column, direction = 'ASC'] = orderGroup.split(',');
return [Sequelize.col(column), direction];
});
} if (attributes && isString(attributes)) {
options.attributes = attributes.split(',');
} else if (attributes && isArray(attributes)) {
options.attributes = attributes;
} if (attributes && isString(attributes)) {
options.attributes = attributes.split(',');
} else if (attributes && isArray(attributes)) {
options.attributes = attributes;
} if (include && isArray(include)) {
options.include = include.map((includeModel) => {
const { model, as, attributes, ...rest }: any = qs.parse(
includeModel,
';',
'='
);
const include: Include = {
model: db[model],
};
if (as) {
include.as = as;
}
if (attributes) {
include.attributes = attributes.split(',');
}
const otherColumns = omit(rest, [
'page',
'limit',
'order',
'attributes',
'include',
]);
if (otherColumns) {
include.where = otherColumns;
}
return include;
});
} const otherColumns = omit(req.query, [
'page',
'limit',
'order',
'attributes',
'include',
]); if (otherColumns) {
conditions = {
where: otherColumns,
};
} req.sequelize = {
options,
conditions,
};
return next();
};
If there is any middleware you should put into your arsenal it would be this. You can setup any read route to have pagination, conditions, and select only the attributes you need instead of the whole resource.
如果有任何中间件应该放在您的军械库中,那就是这样。 您可以设置任何读取路由以具有分页,条件,并仅选择所需的属性而不是整个资源。
There are some Typescript interfaces you can view in the Github repo that I linked above. This article is already getting too long.
您可以在上面链接的Github存储库中查看一些Typescript接口。 这篇文章已经太久了。
创建资源 (Creating a resource)
export const create = (props) => {
const route = async (req: Request, res: Response, next: NextFunction) => {
const db = req.app.get('db');
const model = db[props.model];
if (!model) {
throw {
status: 404,
message: 'Model not found',
};
}
const results = await model.create(req.body);
req.results = results;
next();
};
return [asyncEndpoint(route), toJson];
};
This middleware will take in a properties object with the model name. It will then look up the model from our db (database) object. If you haven’t setup the model correctly it will throw an error. Otherwise it will perform the same operation as the route in the plain router.
该中间件将使用具有模型名称的属性对象。 然后,它将从我们的db(数据库)对象中查找模型。 如果您没有正确设置模型,则会抛出错误。 否则,它将执行与普通路由器中的路由相同的操作。
阅读资源 (Reading a resource)
export const read = (props) => {
const route = async (req: Request, res: Response, next: NextFunction) => {
const db = req.app.get('db');
const model = db[props.model];
if (!model) {
throw {
status: 404,
message: 'Model not found',
};
}
let results = await model.findAll({
...req.sequelize.conditions,
...req.sequelize.options,
});
req.results = results;
next();
};
return [withSequelize, asyncEndpoint(route), toJson];
};
This has the same functionality as the create function, except since we are reading from the database we can use the withSequelize middleware to help control pagination and conditions.
它具有与create函数相同的功能,但是由于我们正在从数据库中读取数据,因此可以使用withSequelize中间件来帮助控制分页和条件。
For example, if we run this API call:
例如,如果我们运行以下API调用:
http://localhost:3000/api/v1/customers?attributes=id,first_name&first_name=John
The API will only return the customer with the first name of John.
API将仅以John的名字返回客户。
通过ID查找一种资源 (Finding One Resource By Id)
export const findByPk = (props) => {
const { id } = props;
const route = async (req: Request, res: Response, next: NextFunction) => {
const db = req.app.get('db');
const model = db[props.model];
if (!model) {
throw {
status: 404,
message: 'Model not found',
};
}
const results = await model.findByPk(req.params[id], {
...req.sequelize.conditions,
...req.sequelize.options,
});
req.results = results;
next();
};
return [withSequelize, asyncEndpoint(route), toJson];
};
This is similar to the read middleware except it will only find one record and return an object, not an array of objects.
这与读取的中间件类似,不同之处在于它只会找到一条记录并返回一个对象,而不是对象数组。
更新资源 (Updating a resource)
export const update = (props) => {
const { key, path, fields } = props;
const route = async (req: Request, res: Response, next: NextFunction) => {
const db = req.app.get('db');
const model = db[props.model];
if (!model) {
throw {
status: 404,
message: 'Model not found',
};
}
const results = await model.update(req.body, {
where: {
[key]: get(req, path),
},
fields,
});
req.results = results;
next();
};
return [asyncEndpoint(route), toJson];
};
You can start to see we are building generic versions of the same functions from the plain router above. You can use any of these in your router, or one of them. If you don’t need to do anything special, these middleware will greatly speed up your time building an API.
您可以开始看到我们正在从上面的普通路由器构建相同功能的通用版本。 您可以在路由器中使用任何一个,也可以使用其中一个。 如果您不需要做任何特别的事情,这些中间件将极大地缩短您构建API的时间。
删除资源 (Deleting a resource)
export const destroy = (props) => {
const { key, path } = props;
const route = async (req: Request, res: Response, next: NextFunction) => {
const db = req.app.get('db');
const model = db[props.model];
if (!model) {
throw {
status: 404,
message: 'Model not found',
};
}
const results = await model.destroy({
where: {
[key]: get(req, path),
},
});
req.results = results;
next();
};
return [asyncEndpoint(route), toJson];
};
Now that we have some middleware, that can build RESTful API routes for us, let’s end by creating a Sequelize router that can take in all these middleware and build a dynamic API for us.
现在我们有了一些可以为我们构建RESTful API路由的中间件,让我们最后创建一个可以吸收所有这些中间件并为我们构建动态API的Sequelize路由器。
序列路由器 (Sequelize Router)
import express from 'express';
import { validateSchema } from './validateSchema';
import { create, read, findByPk, update, destroy } from './sequelize';const sequelizeRouter = (props) => {
const { model, key = 'id', schemas } = props;
const router = express.Router();
router.get('/', read({ model }));
router.get(`/:${model}Id`, findByPk({ model, id: `${model}Id` }));
router.post('/', validateSchema(schemas.create), create({ model }));
router.put(
`/:${model}Id`,
validateSchema(schemas.update),
update({
model,
key,
path: `params.${model}Id`,
})
);
router.delete(
`/:${model}Id`,
destroy({
model,
key,
path: `params.${model}Id`,
})
);
return router;
};export default sequelizeRouter;
Now we have this final middleware we can rewrite our plain customers router like this:
现在我们有了最终的中间件,我们可以像这样重写普通客户路由器:
import joi from '@hapi/joi';
import sequelizeRouter from '../../../../../middleware/sequelizeRouter';const createSchema = {
path: 'body',
schema: joi.object().keys({
first_name: joi.string().required(),
last_name: joi.string().required(),
}),
};const updateSchema = {
path: 'body',
schema: joi.object().keys({
first_name: joi.string().required(),
last_name: joi.string().required(),
}),
};const router = sequelizeRouter({
model: 'Customers',
schemas: { create: createSchema, update: updateSchema },
});export default router;
You can now see that we can do all the heavy lifting through our middleware, the only thing we need to care about now is the schema allowing for creating and updating, and the name of the model. If you don’t care about schema validation then all you need is:
现在您可以看到,我们可以通过中间件完成所有繁重的工作,我们现在唯一需要关心的是允许创建和更新的架构以及模型的名称。 如果您不关心模式验证,那么您需要做的是:
const router = sequelizeRouter({
model: 'Customers',
});
So by just passing the model name we can build an entire RESTful API in a few lines of code.
因此,只需传递模型名称,我们就可以用几行代码构建一个完整的RESTful API。
If you do need to do anything more robust or flexible you can easily break away from the functions and do you own thing. However by keeping the API predictable, consistent, and scalable, you can build more on top of this, such as adding caching and event handling.
如果您确实需要做一些更强大或更灵活的事情,则可以轻松脱离这些功能,并拥有自己的东西。 但是,通过保持API可预测,一致和可扩展,您可以在此基础上进行更多构建,例如添加缓存和事件处理。
结论 (Conclusion)
Writing code that does all the heavy lifting is beneficial when you use the same patterns over and over again. Creating dynamic middleware to generate code for you makes it so easy to implement new features across your API. We created each middleware to handle the HTTP methods and built out an Express router for us.
当您一遍又一遍地使用相同的模式时,编写完成所有繁重工作的代码将非常有益。 通过创建动态中间件来为您生成代码,可以轻松地在整个API中实现新功能。 我们创建了每个中间件来处理HTTP方法,并为我们构建了Express路由器。
Let me know what you think of this approach. Do you like hand coding plain routers all the time? Are there different ways of doing the heavy lifting that you have come across?
让我知道您对这种方法的看法。 您是否一直喜欢手工编码普通路由器? 是否有不同的方式来完成您遇到的繁重工作?