开始使用 AWS 无服务器:强大的 Lambda 类型!

2025-06-11

开始使用 AWS 无服务器:强大的 Lambda 类型!

TL;DR

在本系列中,我将尝试讲解 AWS 上无服务器的基础知识,以便您构建自己的无服务器应用程序。在上一篇文章中,我向您展示了如何在 AWS 上部署前端,以及如何使其与无服务器后端进行交互。

你可能注意到了,我们的前端和后端之间没有共享任何类型:我们没有充分利用前端和后端使用相同语言的优势。在本文中,我们将了解如何在前端和后端之间共享类型,如何使用它们来增强代码的健壮性,以及如何使用契约来加速开发!

⚠️ 本文是上一篇的后续。如果你还没读过上一篇,我强烈建议你在阅读本文之前先读一下!你可以在这里找到我之前的代码。

⬇️ 我会定期发布无服务器内容,如果你想了解更多 ⬇️

在 Twitter 上关注我🚀

今天我们要做什么?

  • 创建合同以在我们的前端和后端之间共享类型
  • 使用它们可以更快、更稳健地定义 lambda 函数
  • 使用它们可以更快、更稳健地从我们的前端进行 API 调用

如果您理解有困难,可以在此处文章末尾找到代码

快速公告:我还在开发一个名为🛡 sls-mentor 🛡的库。它汇集了 30 条无服务器最佳实践,这些实践会在您的 AWS 无服务器项目中自动检查(无论使用哪种框架)。它是免费开源的,欢迎随时查看!

在 Github 上查找 sls-mentor⭐️

我们上次离开哪儿了?

上次,我们创建了一个简单的后端,包含两个 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
Enter fullscreen mode Exit fullscreen mode

但是如何创建合约呢?我所知道的最好的库是@swarmion/serverless-contracts ,由Swarmion团队创建。它导出了非常有用的实用程序,使我们创建的合约在 AWS 无服务器应用程序范围内非常强大。阅读本文,您就会明白其中的原因!

让我们在合约包中安装该库:

npm install @swarmion/serverless-contracts -S
Enter fullscreen mode Exit fullscreen mode

现在,让我们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
  },
});
Enter fullscreen mode Exit fullscreen mode

让我们分解一下这段代码:

  • 前 4 个键用于定义我们合同的路线:路径、方法、集成类型(restApi 或 httpApi)。
  • outputSchemas用于定义路由的输出。在本例中,我们定义路由的输出为 200 响应,其中包含一个带有users键的对象,该对象包含一个用户数组。每个用户都是一个包含firstNamelastName和 的对象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,
  },
});
Enter fullscreen mode Exit fullscreen mode

与上一个合约类似,我们定义了与 API 路径相关的信息、路由的输出,并使用bodySchema键定义了路由的输入。此处,请求的主体将是一个包含emailfirstName和 的对象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
Enter fullscreen mode Exit fullscreen mode

然后,让我们重构我们的 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());
Enter fullscreen mode Exit fullscreen mode

如果将此代码片段与重构前的代码进行比较,您会发现几乎没有任何变化。唯一的区别是我们使用该getHandler函数来生成处理程序。这使得我们的处理程序现在可以根据传递给它的契约完全验证 lambda 函数的输入和输出。此外,不再需要执行 JSON.parse 和 JSON.stringify 操作,因为处理程序现在会为我们完成这些操作!

我还决定使用middy库为我们的 lambda 函数添​​加 CORS 管理功能。这样我们就可以从前端调用 lambda 函数,而不必担心 CORS 问题。

请注意,我禁用了 Lambda 函数的输入和输出验证。目前,Lambda 函数不会在运行时验证其输入和输出是否正确。如果您希望它这样做,可以从选项中移除validateInputvalidateOutput键,而是将一个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());
Enter fullscreen mode Exit fullscreen mode

在这里,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));
Enter fullscreen mode Exit fullscreen mode

在前端使用无服务器合约

合约的第二个非常有用的地方是前端。事实上,我们可以用它们来生成对后端的 API 调用。

首先,让我们在前端包中安装该库:

npm install @swarmion/serverless-contracts -S
Enter fullscreen mode Exit fullscreen mode

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

虽然在文章中看不到,但 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();
};
Enter fullscreen mode Exit fullscreen mode

我们不必指定它是一个 POST 请求,并且根据我们传递给getFetchRequest函数的合同,请求的主体是正确输入的。

结论

在本文中,我们了解了如何使用契约在前端和后端之间共享类型,以及如何使用它们使我们的代码更健壮、编写速度更快:

  • Lambda 函数的输入和输出的类型验证
  • 不再需要解析请求主体
  • 不再需要在前端指定路由路径或 HTTP 方法
  • ETC...

我们可以用合同做很多其他的事情,例如:

  • 生成 API 的 Swagger 文档
  • 对 lambda 函数的输入和输出进行运行时验证
  • 使用来自其他服务(例如 EventBridge)的合约

⭐️ 如果你喜欢这篇文章,那就去Github 上的Swarmion 代码库点个大星吧,他们真的值得!⭐️

您可以在此处找到本文的代码

让我们联系起来!

如果您能回复并分享这篇文章给您的朋友和同事,我将不胜感激。这将极大地帮助我扩大读者群。另外,别忘了订阅,以便及时收到下一篇文章的更新!

如果您想与我保持联系,请访问我的Twitter 账号。我经常发布或转发有关 AWS 和无服务器的有趣内容,欢迎关注我!

在 Twitter 上关注我🚀

鏂囩珷鏉ユ簮锛�https://dev.to/slsbytheodo/learn-serverless-on-aws-step-by-step-strong-types-213i
PREV
JavaScript 数组:像我五岁一样解释
NEXT
使用 VanillaJS 构建自定义 SPA 路由器 简介窗口 - 历史记录和位置对象 实现路由器 结论 RouteNow