开始使用 AWS 无服务器:强大的 Lambda 类型!
TL;DR
在本系列中,我将尝试讲解 AWS 上无服务器的基础知识,以便您构建自己的无服务器应用程序。在上一篇文章中,我向您展示了如何在 AWS 上部署前端,以及如何使其与无服务器后端进行交互。
你可能注意到了,我们的前端和后端之间没有共享任何类型:我们没有充分利用前端和后端使用相同语言的优势。在本文中,我们将了解如何在前端和后端之间共享类型,如何使用它们来增强代码的健壮性,以及如何使用契约来加速开发!
⚠️ 本文是上一篇的后续。如果你还没读过上一篇,我强烈建议你在阅读本文之前先读一下!你可以在这里找到我之前的代码。
⬇️ 我会定期发布无服务器内容,如果你想了解更多 ⬇️
今天我们要做什么?
- 创建合同以在我们的前端和后端之间共享类型
- 使用它们可以更快、更稳健地定义 lambda 函数
- 使用它们可以更快、更稳健地从我们的前端进行 API 调用
如果您理解有困难,可以在此处文章末尾找到代码。
快速公告:我还在开发一个名为🛡 sls-mentor 🛡的库。它汇集了 30 条无服务器最佳实践,这些实践会在您的 AWS 无服务器项目中自动检查(无论使用哪种框架)。它是免费开源的,欢迎随时查看!
我们上次离开哪儿了?
上次,我们创建了一个简单的后端,包含两个 Lambda 函数:createUser 和 listUsers。这两个 Lambda 函数用于与一个 API 和一个 DynamoDB 表进行交互。我们还创建了一个前端,用于调用后端来创建和列出用户。
该架构如下所示:
该存储库如下所示:
在代码方面,我们的 lambda 函数的处理程序类型很差:每个东西都是硬编码的,后端和前端之间没有关系:代码很难维护(多个事实来源)。
使用合同在我们的前端和后端之间共享类型
什么是契约?契约是一段代码,用于定义应用程序两个部分之间的接口。在我们的例子中,我们将使用契约来定义前端和后端之间的接口。我们的两条路由将由契约定义,其中包含以下信息:
- 路线的路径
- 路由的 HTTP 方法
- 路线输入
- 路线的输出
这些信息将提供给我们的前端和后端,以便我们可以使用它来使我们的代码更加健壮和编写更快。
创建无服务器合约
我们希望我们的合同能够适用于许多包,因此利用 NX,我们所要做的就是创建一个名为的新包contracts
,它将在我们的前端和后端之间共享。
cd packages
mkdir contracts && cd contracts
npm init -y # We create a new package, and we initialize it
cp ../backend/tsconfig.json ./tsconfig.json # We re-use the TS config of our backend
但是如何创建合约呢?我所知道的最好的库是@swarmion/serverless-contracts ,由Swarmion团队创建。它导出了非常有用的实用程序,使我们创建的合约在 AWS 无服务器应用程序范围内非常强大。阅读本文,您就会明白其中的原因!
让我们在合约包中安装该库:
npm install @swarmion/serverless-contracts -S
现在,让我们index.ts
在合同包中创建一个文件,并创建我们的第一个合同:
import { ApiGatewayContract } from '@swarmion/serverless-contracts';
export const listUsersContract = new ApiGatewayContract({
id: 'listUsers',
path: '/users',
method: 'GET',
integrationType: 'restApi',
outputSchemas: {
[200]: {
type: 'object',
properties: {
users: {
type: 'array',
items: {
type: 'object',
properties: {
firstName: { type: 'string' },
lastName: { type: 'string' },
email: { type: 'string' },
},
required: ['firstName', 'lastName', 'email'],
additionalProperties: false,
},
},
},
required: ['users'],
additionalProperties: false,
} as const, // This as const is important, it will make sure that the type of the object is inferred correctly
},
});
让我们分解一下这段代码:
- 前 4 个键用于定义我们合同的路线:路径、方法、集成类型(restApi 或 httpApi)。
- 键
outputSchemas
用于定义路由的输出。在本例中,我们定义路由的输出为 200 响应,其中包含一个带有users
键的对象,该对象包含一个用户数组。每个用户都是一个包含firstName
、lastName
和 的对象email
。
您可以根据需要定义任意数量的输出,以适应不同的 HTTP 状态码。例如,这可以让您在前端正确处理错误。
用于定义输出的语法称为JSON Schema。它是定义 JSON 对象结构的标准方法。如果您了解zod,其原理与之类似。根据 Swarmion 的路线图,未来可以使用 zod Schema 来定义合约,这将非常酷!
让我们在同一个文件中添加 createUser 的合约:
// Previous code
export const createUserContract = new ApiGatewayContract({
id: 'createUser',
path: '/users',
method: 'POST',
integrationType: 'restApi',
bodySchema: {
type: 'object',
properties: {
email: { type: 'string' },
firstName: { type: 'string' },
lastName: { type: 'string' },
},
additionalProperties: false,
required: ['email', 'firstName', 'lastName'],
} as const,
outputSchemas: {
[200]: {
type: 'object',
properties: {
message: { type: 'string' },
},
additionalProperties: false,
required: ['message'],
} as const,
},
});
与上一个合约类似,我们定义了与 API 路径相关的信息、路由的输出,并使用bodySchema
键定义了路由的输入。此处,请求的主体将是一个包含email
、firstName
和 的对象lastName
,不允许添加任何其他属性。
我们已经完成了合约的创建!🎉现在,是时候重构我们的后端和前端来使用它们了,看看它们能如何帮助我们!
使用无服务器合约定义 lambda 函数
除了定义合约的方法之外,Swarmion 还公开了其杀手级功能:getHandler
函数。该函数会根据您传递给它的合约,生成一个完全类型的 lambda 处理程序。让我们看看它是如何工作的!
首先,让我们在后端包中安装该库:
npm install @swarmion/serverless-contracts -S
npm install @middy/core @middy/http-cors -S # Will be useful for CORS managementt
然后,让我们重构我们的 lambda 函数来使用我们创建的合约:
import { DynamoDBClient, QueryCommand } from '@aws-sdk/client-dynamodb';
import { listUsersContract } from '../../../contracts';
import { getHandler } from '@swarmion/serverless-contracts';
import middy from '@middy/core';
import cors from '@middy/http-cors';
const client = new DynamoDBClient({});
const main = getHandler(listUsersContract, {
validateInput: false,
validateOutput: false,
returnValidationErrors: false,
})(async () => {
const tableName = process.env.TABLE_NAME;
if (!tableName) {
throw new Error('Missing TABLE_NAME');
}
const { Items } = await client.send(
new QueryCommand({
TableName: tableName,
KeyConditions: {
PK: {
ComparisonOperator: 'EQ',
AttributeValueList: [{ S: 'USER' }],
},
},
}),
);
const users = (Items ?? []).map(item => ({
email: item.SK.S ?? '',
firstName: item.firstName.S ?? '',
lastName: item.lastName.S ?? '',
}));
return {
statusCode: 200,
body: {
users,
},
};
});
export const handler = middy(main).use(cors());
如果将此代码片段与重构前的代码进行比较,您会发现几乎没有任何变化。唯一的区别是我们使用该getHandler
函数来生成处理程序。这使得我们的处理程序现在可以根据传递给它的契约完全验证 lambda 函数的输入和输出。此外,不再需要执行 JSON.parse 和 JSON.stringify 操作,因为处理程序现在会为我们完成这些操作!
我还决定使用middy库为我们的 lambda 函数添加 CORS 管理功能。这样我们就可以从前端调用 lambda 函数,而不必担心 CORS 问题。
请注意,我禁用了 Lambda 函数的输入和输出验证。目前,Lambda 函数不会在运行时验证其输入和输出是否正确。如果您希望它这样做,可以从选项中移除validateInput
和validateOutput
键,而是将一个ajv
实例传递给该ajv
键。这将使 Lambda 函数在运行时验证其输入和输出,并在输入或输出不正确时抛出错误(更多信息请参阅 Swarmion 文档)。
createUser 的处理程序非常相似:
import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb';
import { getHandler } from '@swarmion/serverless-contracts';
import { createUserContract } from '../../../contracts';
import middy from '@middy/core';
import cors from '@middy/http-cors';
const client = new DynamoDBClient({});
const main = getHandler(createUserContract, {
validateInput: false,
validateOutput: false,
returnValidationErrors: false,
})(async event => {
const tableName = process.env.TABLE_NAME;
if (!tableName) {
throw new Error('Missing TABLE_NAME');
}
const { email, firstName, lastName } = event.body;
await client.send(
new PutItemCommand({
TableName: tableName,
Item: {
PK: { S: 'USER' },
SK: { S: email },
firstName: { S: firstName },
lastName: { S: lastName },
},
}),
);
return {
statusCode: 200,
body: {
message: 'User created',
},
};
});
export const handler = middy(main).use(cors());
在这里,Swarmion 合约的真正威力在于,我可以直接从event
对象访问请求主体,而无需对其进行解析。根据我们传递给getHandler
函数的合约,请求主体也是完全类型化的。
在基础设施即代码中使用无服务器合约
现在我们有了合约,就可以用它们来定义我们的基础设施即代码(CDK 代码)。这将确保 Lambda 函数配置在正确的路径上,并使用正确的 HTTP 方法。
在我的文件中backend-stack.ts
,我可以简化一些代码来写:
api.root.resourceForPath(createUserContract.path).addCorsPreflight({
allowOrigins: cdk.aws_apigateway.Cors.ALL_ORIGINS,
allowMethods: cdk.aws_apigateway.Cors.ALL_METHODS,
allowHeaders: cdk.aws_apigateway.Cors.DEFAULT_HEADERS,
});
api.root
.resourceForPath(listUsersContract.path)
.addMethod(listUsersContract.method, new cdk.aws_apigateway.LambdaIntegration(listUsers));
api.root
.resourceForPath(createUserContract.path)
.addMethod(createUserContract.method, new cdk.aws_apigateway.LambdaIntegration(createUser));
在前端使用无服务器合约
合约的第二个非常有用的地方是前端。事实上,我们可以用它们来生成对后端的 API 调用。
首先,让我们在前端包中安装该库:
npm install @swarmion/serverless-contracts -S
fetch
然后,在我们的前端,我们可以用从合约生成的调用替换旧的调用:
import { getFetchRequest } from '@swarmion/serverless-contracts';
import { createUserContract, listUsersContract } from '../../contracts';
// ... rest of the component
const syncUsers = async () => {
const {
body: { users },
} = await getFetchRequest(listUsersContract, fetch, {
baseUrl: import.meta.env.VITE_API_URL,
});
setUsers(users);
};
虽然在文章中看不到,但 users 变量的类型是正确的:它是一个用户数组,正如合约中定义的一样。而且我们不需要指定路由的路径或 HTTP 方法!
表单的提交也一样:
const onFormSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
await getFetchRequest(createUserContract, fetch, {
baseUrl: import.meta.env.VITE_API_URL,
body: {
firstName,
lastName,
email,
},
});
setUsers([...users, { firstName, lastName, email }]);
await syncUsers();
};
我们不必指定它是一个 POST 请求,并且根据我们传递给getFetchRequest
函数的合同,请求的主体是正确输入的。
结论
在本文中,我们了解了如何使用契约在前端和后端之间共享类型,以及如何使用它们使我们的代码更健壮、编写速度更快:
- Lambda 函数的输入和输出的类型验证
- 不再需要解析请求主体
- 不再需要在前端指定路由路径或 HTTP 方法
- ETC...
我们可以用合同做很多其他的事情,例如:
- 生成 API 的 Swagger 文档
- 对 lambda 函数的输入和输出进行运行时验证
- 使用来自其他服务(例如 EventBridge)的合约
⭐️ 如果你喜欢这篇文章,那就去Github 上的Swarmion 代码库点个大星吧,他们真的值得!⭐️
您可以在此处找到本文的代码。
让我们联系起来!
如果您能回复并分享这篇文章给您的朋友和同事,我将不胜感激。这将极大地帮助我扩大读者群。另外,别忘了订阅,以便及时收到下一篇文章的更新!
如果您想与我保持联系,请访问我的Twitter 账号。我经常发布或转发有关 AWS 和无服务器的有趣内容,欢迎关注我!
鏂囩珷鏉ユ簮锛�https://dev.to/slsbytheodo/learn-serverless-on-aws-step-by-step-strong-types-213i