tRPC
tRPC 是一个用于在 TypeScript 中构建 API 的框架,具有端到端的类型安全性。使用 tRPC,对 API 操作输入和输出的更新会立即反映在客户端代码中,并在 IDE 中可见,无需重新构建项目。
tRPC API 生成器创建一个新的 tRPC API,并配置 AWS CDK 或 Terraform 基础设施。生成的后端使用 AWS Lambda 进行无服务器部署,通过 AWS API Gateway API 公开,并包括使用 Zod 进行模式验证。它设置了 AWS Lambda Powertools 以实现可观测性,包括日志记录、AWS X-Ray 跟踪和 Cloudwatch 指标。
生成 tRPC API
Section titled “生成 tRPC API”您可以通过两种方式生成新的 tRPC API:
pnpm nx g @aws/nx-plugin:ts#api --framework=trpcyarn nx g @aws/nx-plugin:ts#api --framework=trpcnpx nx g @aws/nx-plugin:ts#api --framework=trpcbunx nx g @aws/nx-plugin:ts#api --framework=trpc您还可以执行试运行以查看哪些文件会被更改
pnpm nx g @aws/nx-plugin:ts#api --framework=trpc --dry-runyarn nx g @aws/nx-plugin:ts#api --framework=trpc --dry-runnpx nx g @aws/nx-plugin:ts#api --framework=trpc --dry-runbunx nx g @aws/nx-plugin:ts#api --framework=trpc --dry-run- 安装 Nx Console VSCode Plugin 如果您尚未安装
- 在VSCode中打开Nx控制台
- 点击
Generate (UI)在"Common Nx Commands"部分 - 搜索
@aws/nx-plugin - ts#api - 填写必需参数
- framework: trpc
- 点击
Generate
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| name 必需 | string | - | API 的名称(必填)。用于生成类名和文件路径。 |
| framework | trpc | smithy | trpc | 要使用的API框架。 |
| namespace | string | - | Smithy API 的命名空间(仅适用于 smithy 框架)。默认为您的 monorepo 作用域 |
| integrationPattern | isolated | shared | isolated | API Gateway 集成的生成方式。可选择 isolated(默认)或 shared。 |
| auth | iam | cognito | custom | iam | 用于对 API 进行身份验证的方法。可选择 iam(默认)、cognito 或 custom。 |
| directory | string | packages | 存储应用程序的目录。 |
| subDirectory | string | - | 项目所在的子目录。默认情况下为项目名称。 |
| iac | inherit | cdk | terraform | inherit | 首选的 IaC 提供商。默认情况下,这将继承您的初始选择。 |
| infra | rest-lambda | http-lambda | none | rest-lambda | 用于部署此 API 的基础设施类型。 |
| preferInstallDependencies | boolean | true | 是否在生成器运行后优先安装依赖项。设置为 false 可在批量运行多个生成器时延迟安装(如果后续生成器需要计算 Nx 项目图,仍会运行安装);在最后统一安装一次。 |
生成器将在 <directory>/<api-name> 目录中创建以下项目结构:
文件夹src
- init.ts 后端 tRPC 初始化
- handler.ts Lambda 处理程序入口点
- router.ts tRPC 路由器定义
文件夹schema 使用 Zod 的模式定义
- echo.ts “echo” 过程的输入和输出示例定义
- z-async-iterable.ts 用于订阅的 Zod 辅助工具(仅限 REST API)
文件夹procedures API 公开的过程(或操作)
- echo.ts 示例过程
文件夹middleware
- error.ts 用于错误处理的中间件
- logger.ts 用于配置 AWS Powertools for Lambda 日志记录的中间件
- tracer.ts 用于配置 AWS Powertools for Lambda 跟踪的中间件
- metrics.ts 用于配置 AWS Powertools for Lambda 指标的中间件
- local-server.ts 用于本地开发服务器的 tRPC 独立适配器入口点
文件夹client
- index.ts 用于机器对机器 API 调用的类型安全客户端
- tsconfig.json TypeScript 配置
- package.json 定义项目包名称和依赖项的项目清单
- project.json 项目配置和构建目标
由于此生成器根据您选择的 iac 提供基础设施即代码,它将在 packages/common 中创建一个项目,其中包含相关的 CDK 构造或 Terraform 模块。
通用基础设施即代码项目的结构如下:
文件夹packages/common/constructs
文件夹src
文件夹app/ Constructs for infrastructure specific to a project/generator
- …
文件夹core/ Generic constructs which are reused by constructs in
app- …
- index.ts Entry point exporting constructs from
app
- project.json Project build targets and configuration
文件夹packages/common/terraform
文件夹src
文件夹app/ Terraform modules for infrastructure specific to a project/generator
- …
文件夹core/ Generic modules which are reused by modules in
app- …
- project.json Project build targets and configuration
为了部署您的 API,会生成以下文件:
文件夹packages/common/constructs/src
文件夹app
文件夹apis
- <project-name>.ts CDK construct for deploying your API
文件夹core
文件夹api
- http-api.ts CDK construct for deploying an HTTP API (if you selected to deploy an HTTP API)
- rest-api.ts CDK construct for deploying a REST API (if you selected to deploy a REST API)
- utils.ts Utilities for the API constructs
文件夹packages/common/terraform/src
文件夹app
文件夹apis
文件夹<project-name>
- <project-name>.tf Module for deploying your API
文件夹core
文件夹api
文件夹http-api
- http-api.tf Module for deploying an HTTP API (if you selected to deploy an HTTP API)
文件夹rest-api
- rest-api.tf Module for deploying a REST API (if you selected to deploy a REST API)
已部署的应用程序具有以下架构:
REST API 在 API Gateway 阶段前包含一个 AWS WAFv2 Web ACL,并启用了 AWS 托管的默认规则集。
HTTP API 不直接支持 WAF——如果您需要 WAF 保护,请选择 REST API,或者在 HTTP API 前面使用 CloudFront 分配。
实现您的 tRPC API
Section titled “实现您的 tRPC API”在高层次上,tRPC API 由一个路由器组成,该路由器将请求委托给特定的过程。每个过程都有一个输入和输出,定义为 Zod 模式。
src/schema 目录包含在客户端和服务器代码之间共享的类型。在此包中,这些类型使用 Zod 定义,这是一个 TypeScript 优先的模式声明和验证库。
示例模式可能如下所示:
import { z } from 'zod';
// Schema definitionexport const UserSchema = z.object({ name: z.string(), height: z.number(), dateOfBirth: z.string().datetime(),});
// Corresponding TypeScript typeexport type User = z.TypeOf<typeof UserSchema>;给定上述模式,User 类型等效于以下 TypeScript:
interface User { name: string; height: number; dateOfBirth: string;}模式由服务器和客户端代码共享,提供了一个单一的位置,可以在对 API 中使用的结构进行更改时进行更新。
模式在运行时由您的 tRPC API 自动验证,这节省了在后端手工编写自定义验证逻辑的工作。
Zod 提供了强大的实用程序来组合或派生模式,例如 .merge、.pick、.omit 等。您可以在 Zod 文档网站上找到更多信息。
路由器和过程
Section titled “路由器和过程”您的 tRPC 路由器在 src/router.ts 中定义,它注册所有过程。每个过程定义预期的输入、输出和实现。Lambda 处理程序入口点在 src/handler.ts 中,它将请求转发到您的路由器。
为您生成的示例路由器有一个名为 echo 的操作:
import { echo } from './procedures/echo.js';
export const appRouter = router({ echo,});示例 echo 过程在 src/procedures/echo.ts 中为您生成:
export const echo = publicProcedure .input(EchoInputSchema) .output(EchoOutputSchema) .query((opts) => ({ message: opts.input.message }));分解上述内容:
publicProcedure在 API 上定义一个公共方法,包括在src/middleware中设置的中间件。此中间件包括用于日志记录、跟踪和指标的 AWS Lambda Powertools 集成。input接受一个 Zod 模式,该模式定义操作的预期输入。为此操作发送的请求会自动根据此模式进行验证。output接受一个 Zod 模式,该模式定义操作的预期输出。如果您没有返回符合模式的输出,您将在实现中看到类型错误。query接受一个函数,该函数定义 API 的实现。此实现接收opts,其中包含传递给操作的input,以及由中间件设置的其他上下文,可在opts.ctx中使用。传递给query的函数必须返回符合output模式的输出。
使用 query 定义实现表示该操作不是可变的。使用它来定义检索数据的方法。要实现可变操作,请改用 mutation 方法。
如果您添加新过程,请确保通过将其添加到 src/router.ts 中的路由器来注册它。
订阅(流式传输)
Section titled “订阅(流式传输)”tRPC 订阅允许您使用服务器发送事件 (SSE) 从服务器向客户端流式传输数据。当您选择 rest-lambda 作为计算类型时,生成器会自动配置流式传输所需的基础设施,以及流式传输 Lambda 处理程序和 ZodAsyncIterable 模式辅助工具。
要定义订阅过程,请使用带有异步生成器函数的 .subscription 方法。使用 src/schema/z-async-iterable.ts 中的 ZodAsyncIterable 辅助工具来定义输出模式:
import { publicProcedure } from '../init.js';import { z } from 'zod';import { ZodAsyncIterable } from '../schema/z-async-iterable.js';
const InputSchema = z.object({ query: z.string() });const ChunkSchema = z.object({ text: z.string() });
export const myStream = publicProcedure .input(InputSchema) .output( ZodAsyncIterable({ yield: ChunkSchema, }), ) .subscription(async function* (opts) { // Yield data to the client as it becomes available for (const chunk of await getResults(opts.input.query)) { yield { text: chunk }; } });像注册任何其他过程一样在路由器中注册订阅:
export const appRouter = router({ echo, myStream,});生成的基础设施对所有 REST API 操作使用带有 API Gateway 中 ResponseTransferMode.STREAM 的流式传输 Lambda 处理程序,这使得订阅可以与常规查询和变更一起工作。
自定义您的 tRPC API
Section titled “自定义您的 tRPC API”在您的实现中,您可以通过抛出 TRPCError 向客户端返回错误响应。这些接受一个 code,指示错误类型,例如:
throw new TRPCError({ code: 'NOT_FOUND', message: 'The requested resource could not be found',});组织您的操作
Section titled “组织您的操作”随着 API 的增长,您可能希望将相关操作分组在一起。
您可以使用嵌套路由器将操作分组在一起,例如:
import { getUser } from './procedures/users/get.js';import { listUsers } from './procedures/users/list.js';
const appRouter = router({ users: router({ get: getUser, list: listUsers, }), ...})然后,客户端会收到这种操作分组,例如,在这种情况下调用 listUsers 操作可能如下所示:
client.users.list.query();AWS Lambda Powertools 日志记录器在 src/middleware/logger.ts 中配置,可以通过 opts.ctx.logger 在 API 实现中访问。您可以使用它来记录到 CloudWatch Logs,和/或控制要包含在每个结构化日志消息中的附加值。例如:
export const echo = publicProcedure .input(...) .output(...) .query(async (opts) => { opts.ctx.logger.info('Operation called with input', opts.input);
return ...; });有关日志记录器的更多信息,请参阅 AWS Lambda Powertools Logger 文档。
AWS Lambda Powertools 指标在 src/middleware/metrics.ts 中配置,可以通过 opts.ctx.metrics 在 API 实现中访问。您可以使用它在 CloudWatch 中记录指标,而无需导入和使用 AWS SDK,例如:
export const echo = publicProcedure .input(...) .output(...) .query(async (opts) => { opts.ctx.metrics.addMetric('Invocations', 'Count', 1);
return ...; });有关更多信息,请参阅 AWS Lambda Powertools Metrics 文档。
微调 X-Ray 跟踪
Section titled “微调 X-Ray 跟踪”AWS Lambda Powertools 跟踪器在 src/middleware/tracer.ts 中配置,可以通过 opts.ctx.tracer 在 API 实现中访问。您可以使用它通过 AWS X-Ray 添加跟踪,以提供对 API 请求的性能和流程的详细洞察。例如:
export const echo = publicProcedure .input(...) .output(...) .query(async (opts) => { const subSegment = opts.ctx.tracer.getSegment()!.addNewSubsegment('MyAlgorithm'); // ... my algorithm logic to capture subSegment.close();
return ...; });有关更多信息,请参阅 AWS Lambda Powertools Tracer 文档。
实现自定义中间件
Section titled “实现自定义中间件”您可以通过实现中间件向提供给过程的上下文添加附加值。
例如,让我们在 src/middleware/identity.ts 中实现一些中间件,以从我们的 API 中提取有关调用用户的一些详细信息。
此示例演示了 IAM 身份验证的身份中间件。我们使用从 API Gateway 事件中提取的 sub 在 Cognito 中查找调用者。
首先,我们定义要添加到上下文的内容:
export interface IIdentityContext { identity?: { sub: string; username: string; };}请注意,我们为上下文定义了一个额外的_可选_属性。tRPC 管理确保在正确配置了此中间件的过程中定义此属性。
接下来,我们将实现中间件本身。它具有以下结构:
export const createIdentityPlugin = () => { const t = initTRPC.context<...>().create(); return t.procedure.use(async (opts) => { // Add logic here to run before the procedure
const response = await opts.next(...);
// Add logic here to run after the procedure
return response; });};在我们的情况下,我们想要提取有关调用 Cognito 用户的详细信息。我们将通过从 API Gateway 事件中提取用户的主题 ID(或”sub”)并从 Cognito 检索用户详细信息来实现。实现因事件是由 REST API 还是 HTTP API 提供给我们的函数而异:
import { CognitoIdentityProvider } from '@aws-sdk/client-cognito-identity-provider';import { initTRPC, TRPCError } from '@trpc/server';import { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda';import { APIGatewayProxyEvent } from 'aws-lambda';
export interface IIdentityContext { identity?: { sub: string; username: string; };}
export const createIdentityPlugin = () => { const t = initTRPC.context<IIdentityContext & CreateAWSLambdaContextOptions<APIGatewayProxyEvent>>().create();
const cognito = new CognitoIdentityProvider();
return t.procedure.use(async (opts) => { const cognitoAuthenticationProvider = opts.ctx.event.requestContext?.identity?.cognitoAuthenticationProvider;
let sub: string | undefined = undefined; if (cognitoAuthenticationProvider) { const providerParts = cognitoAuthenticationProvider.split(':'); sub = providerParts[providerParts.length - 1]; }
if (!sub) { throw new TRPCError({ code: 'FORBIDDEN', message: `Unable to determine calling user`, }); }
const { Users } = await cognito.listUsers({ // Assumes user pool id is configured in lambda environment UserPoolId: process.env.USER_POOL_ID!, Limit: 1, Filter: `sub="${sub}"`, });
if (!Users || Users.length !== 1) { throw new TRPCError({ code: 'FORBIDDEN', message: `No user found with subjectId ${sub}`, }); }
// Provide the identity to other procedures in the context return await opts.next({ ctx: { ...opts.ctx, identity: { sub, username: Users[0].Username!, }, }, }); });};import { CognitoIdentityProvider } from '@aws-sdk/client-cognito-identity-provider';import { initTRPC, TRPCError } from '@trpc/server';import { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda';import { APIGatewayProxyEventV2WithIAMAuthorizer } from 'aws-lambda';
export interface IIdentityContext { identity?: { sub: string; username: string; };}
export const createIdentityPlugin = () => { const t = initTRPC.context<IIdentityContext & CreateAWSLambdaContextOptions<APIGatewayProxyEventV2WithIAMAuthorizer>>().create();
const cognito = new CognitoIdentityProvider();
return t.procedure.use(async (opts) => { const cognitoIdentity = opts.ctx.event.requestContext?.authorizer?.iam ?.cognitoIdentity as unknown as | { amr: string[]; } | undefined;
const sub = (cognitoIdentity?.amr ?? []) .flatMap((s) => (s.includes(':CognitoSignIn:') ? [s] : [])) .map((s) => { const parts = s.split(':'); return parts[parts.length - 1]; })?.[0];
if (!sub) { throw new TRPCError({ code: 'FORBIDDEN', message: `Unable to determine calling user`, }); }
const { Users } = await cognito.listUsers({ // Assumes user pool id is configured in lambda environment UserPoolId: process.env.USER_POOL_ID!, Limit: 1, Filter: `sub="${sub}"`, });
if (!Users || Users.length !== 1) { throw new TRPCError({ code: 'FORBIDDEN', message: `No user found with subjectId ${sub}`, }); }
// Provide the identity to other procedures in the context return await opts.next({ ctx: { ...opts.ctx, identity: { sub, username: Users[0].Username!, }, }, }); });};当您使用 auth: 'cognito' 部署时,API Gateway Cognito 授权器会验证调用者在 Authorization 标头中提供的 JWT,并将验证的声明放在 Lambda 事件上。我们的中间件只是读取这些声明 — 无需额外的 AWS SDK 调用,无需手动 JWT 验证。
首先,我们定义要添加到上下文的内容:
export interface IIdentityContext { identity?: { sub: string; username: string; };}请注意,我们在上下文上定义了一个额外的_可选_属性。tRPC 管理确保在正确配置了此中间件的过程中定义此属性。
接下来是中间件本身:
import { initTRPC, TRPCError } from '@trpc/server';import { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda';import { APIGatewayProxyEvent } from 'aws-lambda';
export interface IIdentityContext { identity?: { sub: string; username: string; };}
export const createIdentityPlugin = () => { const t = initTRPC .context<IIdentityContext & CreateAWSLambdaContextOptions<APIGatewayProxyEvent>>() .create();
return t.procedure.use(async (opts) => { const claims = opts.ctx.event.requestContext?.authorizer?.claims as | Record<string, string> | undefined;
const sub = claims?.sub; const username = claims?.username;
if (!sub || !username) { throw new TRPCError({ code: 'FORBIDDEN', message: 'Unable to determine calling user', }); }
return await opts.next({ ctx: { ...opts.ctx, identity: { sub, username, }, }, }); });};然后,您可以将插件混合到任何需要调用者身份的过程中:
import { publicProcedure } from '../init.js';import { createIdentityPlugin } from '../middleware/identity.js';import { z } from 'zod';
export const me = publicProcedure .concat(createIdentityPlugin()) .output(z.object({ sub: z.string(), username: z.string() })) .query(({ ctx }) => ({ sub: ctx.identity!.sub, username: ctx.identity!.username, }));部署您的 tRPC API
Section titled “部署您的 tRPC API”tRPC API 生成器根据您选择的 iac 创建 CDK 或 Terraform 基础设施即代码。您可以使用它来部署您的 tRPC API。
用于部署 API 的 CDK 构造位于 common/constructs 文件夹中。您可以在 CDK 应用程序中使用它,例如:
import { MyApi } from '@my-scope/common-constructs';
export class ExampleStack extends Stack { constructor(scope: Construct, id: string) { // Add the api to your stack const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), }); }}import { MyApi, UserIdentity } from '@my-scope/common-constructs';
export class ExampleStack extends Stack { constructor(scope: Construct, id: string) { // Add the api to your stack const identity = new UserIdentity(this, 'Identity');
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), identity, }); }}UserIdentity 构造可以使用 ts#website#auth 生成器生成。
这会设置您的 API 基础设施,包括 AWS API Gateway REST 或 HTTP API、用于业务逻辑的 AWS Lambda 函数,以及基于您选择的 auth 方法的身份验证。
用于部署 API 的 Terraform 模块位于 common/terraform 文件夹中。您可以在 Terraform 配置中使用它。
API 模块将其 Lambda 部署 zip 暂存在共享的 S3 资产存储桶中 — 有关详细信息,请参阅 Terraform 基础设施指南。每个部署实例化一次 core/asset-bucket 模块,并通过 asset_bucket_name 输入将其 bucket_name 输出传递给每个 API / Lambda 模块:
module "asset_bucket" { source = "../../common/terraform/src/core/asset-bucket"}
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# Environment variables for the Lambda function env = { ENVIRONMENT = var.environment LOG_LEVEL = "INFO" }
# Additional IAM policies if needed additional_iam_policy_statements = [ # Add any additional permissions your API needs ]
tags = local.common_tags}module "asset_bucket" { source = "../../common/terraform/src/core/asset-bucket"}
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
user_pool_id = local.user_pool_id user_pool_client_ids = [local.client_id]
# Environment variables for the Lambda function env = { ENVIRONMENT = var.environment LOG_LEVEL = "INFO" }
# Additional IAM policies if needed additional_iam_policy_statements = [ # Add any additional permissions your API needs ]
tags = local.common_tags}您可以使用适当的 Terraform 资源或模块设置 Cognito User Pool 和 Client。
这会设置:
- 一个为所有 tRPC 过程提供服务的 AWS Lambda 函数
- API Gateway HTTP/REST API 作为函数触发器
- IAM 角色和权限
- CloudWatch 日志组
- X-Ray 跟踪配置
- CORS 配置
Terraform 模块提供了几个您可以使用的输出:
# Access the API endpointoutput "api_url" { value = module.my_api.stage_invoke_url}
# Access Lambda function detailsoutput "lambda_function_name" { value = module.my_api.lambda_function_name}
# Access IAM role for granting additional permissionsoutput "lambda_execution_role_arn" { value = module.my_api.lambda_execution_role_arn}您可以通过向模块传递变量来自定义 CORS 设置:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# Custom CORS configuration cors_allow_origins = ["https://myapp.com", "https://staging.myapp.com"] cors_allow_methods = ["GET", "POST", "PUT", "DELETE"] cors_allow_headers = [ "authorization", "content-type", "x-custom-header" ]
tags = local.common_tags}对于 REST API,生成的构造默认会将 AWS WAFv2 Web ACL 与 API Gateway 阶段关联。Web ACL 使用 AWS 托管的默认规则集(AWSManagedRulesCommonRuleSet 和 AWSManagedRulesKnownBadInputsRuleSet),提供针对常见 Web 漏洞(包括 OWASP Top 10)的保护。WAF 请求日志会写入 CloudWatch Logs 日志组。
您可以编辑生成的 rest-api 构造来添加、删除或调整规则(例如,添加基于速率的规则或其他托管规则组)。
要选择退出(例如,附加您自己的 Web ACL),请将 enableWaf 设置为 false:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), enableWaf: false,});要选择退出(例如,附加您自己的 Web ACL),请将 enable_waf 设置为 false:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name enable_waf = false}访问日志记录
Section titled “访问日志记录”对于 REST API,生成的基础设施默认启用访问日志,为每个请求向专用的 CloudWatch Logs 日志组写入一行结构化的 JSON。该日志组使用客户管理的 KMS 密钥加密,并保留一年。
API Gateway 使用账户级别的 CloudWatch Logs 角色写入访问日志。此角色在 AWS::ApiGateway::Account 设置上配置,该设置是每个区域每个账户的单例 — 区域中的每个 REST API 只有一个角色。为了在多个独立部署的堆栈中安全地管理此角色,生成的基础设施:
- 仅在尚未设置有效角色时创建共享的 CloudWatch Logs 角色并在账户上配置它,因此部署永远不会覆盖另一个堆栈拥有的角色。
- 在拆除时保持账户设置不变,因此销毁一个堆栈永远不会禁用区域中其他 REST API 的日志记录。
账户角色由 ApiGatewayAccount 构造管理,这是一个通过 ApiGatewayAccount.ensure(scope) 解析的堆栈范围单例。每个 REST API 的阶段都依赖于它,该角色由 Lambda 支持的自定义资源配置。
访问日志格式由您的 API 扩展的 RestApi 构造设置。要自定义它,请在生成的 packages/common/constructs/src/app/apis/my-api.ts 中将 deployOptions 传递给 super,同时保留构造已设置的 tracingEnabled:
super(scope, id, { apiName: 'MyApi', // ... deployOptions: { tracingEnabled: true, accessLogFormat: AccessLogFormat.clf(), }, ...props,});AccessLogFormat 从 aws-cdk-lib/aws-apigateway 导入。任何未设置的内容都会保留构造的默认值 — 一个包含标准字段的 JSON 格式。
账户角色由 core/api/api-gateway-account 模块管理,该模块由生成的 API 模块实例化。它以幂等方式配置账户,并且在 terraform destroy 时永远不会重置。
您可以通过编辑生成的 API 模块中 aws_api_gateway_stage 资源上的 access_log_settings 块来自定义访问日志格式。
REST/HTTP API CDK 构造被配置为提供类型安全的接口,用于为每个操作定义集成。
您可以使用静态方法 defaultIntegrations 来使用默认模式,该模式为每个操作定义一个单独的 AWS Lambda 函数:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});Terraform 模块自动使用路由器模式和单个 Lambda 函数。无需额外配置:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# The module automatically creates a single Lambda function # that handles all API operations tags = local.common_tags}您可以通过 API 构造的 integrations 属性以类型安全的方式访问底层的 AWS Lambda 函数。例如,如果您的 API 定义了一个名为 sayHello 的操作,并且您需要向该函数添加一些权限,可以按如下方式操作:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
// sayHello is typed to the operations defined in your APIapi.integrations.sayHello.handler.addToRolePolicy(new PolicyStatement({ effect: Effect.ALLOW, actions: [...], resources: [...],}));如果您的 API 使用 shared 模式,共享的路由器 Lambda 将作为 api.integrations.$router 公开:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
api.integrations.$router.handler.addEnvironment('LOG_LEVEL', 'DEBUG');使用 Terraform 的路由器模式,只有一个 Lambda 函数。您可以通过模块输出访问它:
# Grant additional permissions to the single Lambda functionresource "aws_iam_role_policy" "additional_permissions" { name = "additional-api-permissions" role = module.my_api.lambda_execution_role_name
policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "s3:GetObject", "s3:PutObject" ] Resource = "arn:aws:s3:::my-bucket/*" } ] })}自定义默认选项
Section titled “自定义默认选项”如果您想自定义创建每个默认集成的 Lambda 函数时使用的选项,可以使用 withDefaultOptions 方法。例如,如果您希望所有 Lambda 函数都驻留在 Vpc 中:
const vpc = new Vpc(this, 'Vpc', ...);
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withDefaultOptions({ vpc, }) .build(),});VPC 配置已由生成的模块支持 — 设置 enable_vpc 以及 vpc_id 和 subnet_ids,模块会将 Lambda 函数部署到您的 VPC 中,并为您创建一个安全组:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# VPC configuration enable_vpc = true vpc_id = aws_vpc.main.id subnet_ids = [aws_subnet.private_a.id, aws_subnet.private_b.id]
tags = local.common_tags}对于模块未公开的选项,请直接编辑生成的 Terraform 模块中的 aws_lambda_function 资源。
按操作自定义选项
Section titled “按操作自定义选项”要自定义用于创建_特定_操作的默认集成的选项(而不影响其他操作),可以使用 withOperationOptions 方法。例如,如果您想仅为一个操作增加 Lambda 函数超时时间:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOperationOptions({ sayHello: { timeout: Duration.seconds(60), }, }) .build(),});
// The selected operations remain default integrations, so they're still typed accordingly:api.integrations.sayHello.handler.addToRolePolicy(new PolicyStatement({ ... }));您指定的选项将与默认集成选项(以及通过 withDefaultOptions 设置的任何选项)合并。请注意,您不能为通过 withOverrides 替换的操作指定选项,因为这些操作不再使用默认集成。
如果同一操作同时被 withOperationOptions 和 withOverrides 针对,无论您调用它们的顺序如何,都会遇到类型错误。
要使用 Terraform 为特定操作自定义选项,您需要编辑生成的 Terraform 模块以配置每个操作的单独 Lambda 函数(请参阅下面的显式集成部分)。
您还可以使用 withOverrides 方法覆盖特定操作的集成。每个覆盖必须指定一个 integration 属性,该属性的类型对应于 HTTP 或 REST API 的适当 CDK 集成构造。withOverrides 方法也是类型安全的。例如,如果您想覆盖 getDocumentation API 以指向某个外部网站托管的文档,可以按如下方式实现:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), }, }) .build(),});您还会注意到,当通过 api.integrations.getDocumentation 访问时,被覆盖的集成不再具有 handler 属性。
您可以向集成添加额外的属性,这些属性也将相应地进行类型化,从而允许其他类型的集成被抽象但保持类型安全。例如,如果您为 REST API 创建了 S3 集成,并且稍后希望引用特定操作的存储桶,可以按如下方式操作:
const storageBucket = new Bucket(this, 'Bucket', { ... });
const apiGatewayRole = new Role(this, 'ApiGatewayS3Role', { assumedBy: new ServicePrincipal('apigateway.amazonaws.com'),});
storageBucket.grantRead(apiGatewayRole);
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getFile: { bucket: storageBucket, integration: new AwsIntegration({ service: 's3', integrationHttpMethod: 'GET', path: `${storageBucket.bucketName}/{fileName}`, options: { credentialsRole: apiGatewayRole, requestParameters: { 'integration.request.path.fileName': 'method.request.querystring.fileName', }, integrationResponses: [{ statusCode: '200' }], }, }), options: { requestParameters: { 'method.request.querystring.fileName': true, }, methodResponses: [{ statusCode: '200', }], } }, }) .build(),});
// Later, perhaps in another file, you can access the bucket property we defined// in a type-safe mannerapi.integrations.getFile.bucket.grantRead(...);您还可以在集成中提供 options 来覆盖特定的方法选项,例如授权器。例如,如果您希望为 getDocumentation 操作使用 Cognito 身份验证:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), options: { authorizer: new CognitoUserPoolsAuthorizer(...) // for REST, or HttpUserPoolAuthorizer for an HTTP API } }, }) .build(),});如果您愿意,可以选择不使用默认集成,而是直接为每个操作提供一个集成。这在以下情况下很有用,例如,每个操作需要使用不同类型的集成,或者您希望在添加新操作时收到类型错误:
new MyApi(this, 'MyApi', { integrations: { sayHello: { integration: new LambdaIntegration(...), }, getDocumentation: { integration: new HttpIntegration(...), }, },});对于使用 Terraform 的显式每个操作集成,您应该修改生成的应用程序特定模块,以用每个操作的特定集成替换默认代理集成。
编辑 packages/common/terraform/src/app/apis/my-api/my-api.tf:
- 删除默认代理路由(例如,
resource "aws_apigatewayv2_route" "proxy_routes") - 用每个操作的单独函数替换单个 Lambda 函数
- 为每个操作创建特定的集成和路由,重用相同的 ZIP 包:
# Remove the default single Lambda function resource "aws_lambda_function" "api_lambda" { s3_bucket = aws_s3_object.lambda_zip.bucket s3_key = aws_s3_object.lambda_zip.key s3_object_version = aws_s3_object.lambda_zip.version_id function_name = "MyApiHandler" role = aws_iam_role.lambda_execution_role.arn handler = "index.handler" runtime = "nodejs22.x" timeout = 30 # ... rest of configuration }
# Remove the default proxy integration resource "aws_apigatewayv2_integration" "lambda_integration" { api_id = module.http_api.api_id integration_type = "AWS_PROXY" integration_uri = aws_lambda_function.api_lambda.invoke_arn # ... rest of configuration }
# Remove the default proxy routes resource "aws_apigatewayv2_route" "proxy_routes" { for_each = toset(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]) api_id = module.http_api.api_id route_key = "${each.key} /{proxy+}" target = "integrations/${aws_apigatewayv2_integration.lambda_integration.id}" # ... rest of configuration }
# Add individual Lambda functions for each operation using the same bundle resource "aws_lambda_function" "say_hello_handler" { s3_bucket = aws_s3_object.lambda_zip.bucket s3_key = aws_s3_object.lambda_zip.key s3_object_version = aws_s3_object.lambda_zip.version_id function_name = "MyApi-SayHello" role = aws_iam_role.lambda_execution_role.arn handler = "sayHello.handler" # Specific handler for this operation runtime = "nodejs22.x" timeout = 30 source_code_hash = data.archive_file.lambda_zip.output_base64sha256
tracing_config { mode = "Active" }
environment { variables = var.env }
tags = var.tags }
resource "aws_lambda_function" "get_documentation_handler" { s3_bucket = aws_s3_object.lambda_zip.bucket s3_key = aws_s3_object.lambda_zip.key s3_object_version = aws_s3_object.lambda_zip.version_id function_name = "MyApi-GetDocumentation" role = aws_iam_role.lambda_execution_role.arn handler = "getDocumentation.handler" # Specific handler for this operation runtime = "nodejs22.x" timeout = 30 source_code_hash = data.archive_file.lambda_zip.output_base64sha256
tracing_config { mode = "Active" }
environment { variables = var.env }
tags = var.tags }
# Add specific integrations for each operation resource "aws_apigatewayv2_integration" "say_hello_integration" { api_id = module.http_api.api_id integration_type = "AWS_PROXY" integration_uri = aws_lambda_function.say_hello_handler.invoke_arn payload_format_version = "2.0" timeout_milliseconds = 30000 }
resource "aws_apigatewayv2_integration" "get_documentation_integration" { api_id = module.http_api.api_id integration_type = "HTTP_PROXY" integration_uri = "https://example.com/documentation" integration_method = "GET" }
# Add specific routes for each operation resource "aws_apigatewayv2_route" "say_hello_route" { api_id = module.http_api.api_id route_key = "POST /sayHello" target = "integrations/${aws_apigatewayv2_integration.say_hello_integration.id}" authorization_type = "AWS_IAM" }
resource "aws_apigatewayv2_route" "get_documentation_route" { api_id = module.http_api.api_id route_key = "GET /documentation" target = "integrations/${aws_apigatewayv2_integration.get_documentation_integration.id}" authorization_type = "NONE" }
# Add Lambda permissions for each function resource "aws_lambda_permission" "say_hello_permission" { statement_id = "AllowExecutionFromAPIGateway-SayHello" action = "lambda:InvokeFunction" function_name = aws_lambda_function.say_hello_handler.function_name principal = "apigateway.amazonaws.com" source_arn = "${module.http_api.api_execution_arn}/*/*" }
resource "aws_lambda_permission" "get_documentation_permission" { statement_id = "AllowExecutionFromAPIGateway-GetDocumentation" action = "lambda:InvokeFunction" function_name = aws_lambda_function.get_documentation_handler.function_name principal = "apigateway.amazonaws.com" source_arn = "${module.http_api.api_execution_arn}/*/*" }# Remove the default single Lambda function resource "aws_lambda_function" "api_lambda" { s3_bucket = aws_s3_object.lambda_zip.bucket s3_key = aws_s3_object.lambda_zip.key s3_object_version = aws_s3_object.lambda_zip.version_id function_name = "MyApiHandler-${random_string.suffix.result}" role = aws_iam_role.lambda_execution_role.arn handler = "index.handler" runtime = "nodejs22.x" timeout = 30 # ... rest of configuration }
# Remove the default proxy integration resource "aws_api_gateway_integration" "lambda_integration" { rest_api_id = module.rest_api.api_id resource_id = aws_api_gateway_resource.proxy_resource.id http_method = aws_api_gateway_method.proxy_method.http_method integration_http_method = "POST" type = "AWS_PROXY" uri = aws_lambda_function.api_lambda.invoke_arn # ... rest of configuration }
# Remove the default catch-all proxy method resource "aws_api_gateway_method" "proxy_method" { rest_api_id = module.rest_api.api_id resource_id = aws_api_gateway_resource.proxy_resource.id http_method = "ANY" # ... rest of configuration }
# Add individual Lambda functions for each operation using the same bundle resource "aws_lambda_function" "say_hello_handler" { s3_bucket = aws_s3_object.lambda_zip.bucket s3_key = aws_s3_object.lambda_zip.key s3_object_version = aws_s3_object.lambda_zip.version_id function_name = "MyApi-SayHello" role = aws_iam_role.lambda_execution_role.arn handler = "sayHello.handler" # Specific handler for this operation runtime = "nodejs22.x" timeout = 30 source_code_hash = data.archive_file.lambda_zip.output_base64sha256
tracing_config { mode = "Active" }
environment { variables = var.env }
tags = var.tags }
resource "aws_lambda_function" "get_documentation_handler" { s3_bucket = aws_s3_object.lambda_zip.bucket s3_key = aws_s3_object.lambda_zip.key s3_object_version = aws_s3_object.lambda_zip.version_id function_name = "MyApi-GetDocumentation" role = aws_iam_role.lambda_execution_role.arn handler = "getDocumentation.handler" # Specific handler for this operation runtime = "nodejs22.x" timeout = 30 source_code_hash = data.archive_file.lambda_zip.output_base64sha256
tracing_config { mode = "Active" }
environment { variables = var.env }
tags = var.tags }
# Add specific resources and methods for each operation resource "aws_api_gateway_resource" "say_hello_resource" { rest_api_id = module.rest_api.api_id parent_id = module.rest_api.api_root_resource_id path_part = "sayHello" }
resource "aws_api_gateway_method" "say_hello_method" { rest_api_id = module.rest_api.api_id resource_id = aws_api_gateway_resource.say_hello_resource.id http_method = "POST" authorization = "AWS_IAM" }
resource "aws_api_gateway_integration" "say_hello_integration" { rest_api_id = module.rest_api.api_id resource_id = aws_api_gateway_resource.say_hello_resource.id http_method = aws_api_gateway_method.say_hello_method.http_method
integration_http_method = "POST" type = "AWS_PROXY" uri = aws_lambda_function.say_hello_handler.invoke_arn }
resource "aws_api_gateway_resource" "get_documentation_resource" { rest_api_id = module.rest_api.api_id parent_id = module.rest_api.api_root_resource_id path_part = "documentation" }
resource "aws_api_gateway_method" "get_documentation_method" { rest_api_id = module.rest_api.api_id resource_id = aws_api_gateway_resource.get_documentation_resource.id http_method = "GET" authorization = "NONE" }
resource "aws_api_gateway_integration" "get_documentation_integration" { rest_api_id = module.rest_api.api_id resource_id = aws_api_gateway_resource.get_documentation_resource.id http_method = aws_api_gateway_method.get_documentation_method.http_method
integration_http_method = "GET" type = "HTTP" uri = "https://example.com/documentation" }
# Update deployment to depend on new integrations~ resource "aws_api_gateway_deployment" "api_deployment" { rest_api_id = module.rest_api.api_id
depends_on = [ aws_api_gateway_integration.lambda_integration, aws_api_gateway_integration.say_hello_integration, aws_api_gateway_integration.get_documentation_integration, ]
lifecycle { create_before_destroy = true }
triggers = { redeployment = sha1(jsonencode([ aws_api_gateway_integration.say_hello_integration, aws_api_gateway_integration.get_documentation_integration, ])) } }
# Add Lambda permissions for each function resource "aws_lambda_permission" "say_hello_permission" { statement_id = "AllowExecutionFromAPIGateway-SayHello" action = "lambda:InvokeFunction" function_name = aws_lambda_function.say_hello_handler.function_name principal = "apigateway.amazonaws.com" source_arn = "${module.rest_api.api_execution_arn}/*/*" }
resource "aws_lambda_permission" "get_documentation_permission" { statement_id = "AllowExecutionFromAPIGateway-GetDocumentation" action = "lambda:InvokeFunction" function_name = aws_lambda_function.get_documentation_handler.function_name principal = "apigateway.amazonaws.com" source_arn = "${module.rest_api.api_execution_arn}/*/*" }生成的 CDK API 构造支持两种集成模式:
isolated为每个操作创建一个 Lambda 函数。这是生成的 API 的默认设置。shared创建一个默认路由器 Lambda 并将其重用于每个操作,除非您覆盖特定集成。
isolated 为您提供更细粒度的每个操作权限和配置。shared 减少了 Lambda 和 API Gateway 集成的扩散,同时仍允许选择性覆盖。
例如,将 pattern 设置为 'shared' 会创建一个函数,而不是每个集成一个函数:
export class MyApi<...> extends ... {
public static defaultIntegrations = (scope: Construct) => { ... return IntegrationBuilder.rest({ pattern: 'shared', ... }); };}Terraform 模块自动使用路由器模式 - 这是默认且唯一支持的方法。生成的模块创建一个处理所有 API 操作的单个 Lambda 函数。
您可以简单地实例化默认模块以获得路由器模式:
# Default router pattern - single Lambda function for all operationsmodule "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# Single Lambda function handles all operations automatically tags = local.common_tags}授予访问权限(仅限 IAM)
Section titled “授予访问权限(仅限 IAM)”您可以按如下方式授予对 API 的访问权限:
api.grantInvokeAccess(myIdentityPool.authenticatedRole);# Create an IAM policy to allow invoking the APIresource "aws_iam_policy" "api_invoke_policy" { name = "MyApiInvokePolicy" description = "Policy to allow invoking the tRPC API"
policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = "execute-api:Invoke" Resource = "${module.my_api.api_execution_arn}/*/*" } ] })}
# Attach the policy to an IAM role (e.g., for authenticated users)resource "aws_iam_role_policy_attachment" "api_invoke_access" { role = aws_iam_role.authenticated_user_role.name policy_arn = aws_iam_policy.api_invoke_policy.arn}
# Or attach to an existing role by nameresource "aws_iam_role_policy_attachment" "api_invoke_access_existing" { role = "MyExistingRole" policy_arn = aws_iam_policy.api_invoke_policy.arn}您可以用于 IAM 策略的 API 模块的关键输出是:
module.my_api.api_execution_arn- 用于授予 execute-api:Invoke 权限module.my_api.api_arn- API Gateway ARNmodule.my_api.lambda_function_arn- Lambda 函数 ARN
生成器会自动配置一个 bundle 目标,它使用 Rolldown 来创建部署包:
pnpm nx bundle <project-name>yarn nx bundle <project-name>npx nx bundle <project-name>bunx nx bundle <project-name>Rolldown 配置可以在 rolldown.config.ts 中找到,每个要生成的包都有一个入口。Rolldown 会管理并行创建多个包(如果已定义)。
本地 tRPC 服务器
Section titled “本地 tRPC 服务器”您可以使用 serve 目标为您的 API 运行本地服务器,例如:
pnpm nx serve my-apiyarn nx serve my-apinpx nx serve my-apibunx nx serve my-api本地服务器的入口点是 src/local-server.ts。
当您对 API 进行更改时,这将自动重新加载。
调用您的 tRPC API
Section titled “调用您的 tRPC API”您可以创建一个 tRPC 客户端以类型安全的方式调用您的 API。如果您从另一个后端调用 tRPC API,可以使用 src/client/index.ts 中的客户端,例如:
import { createMyApiClient } from '@my-scope/my-api';
const client = createMyApiClient({ url: 'https://my-api-url.example.com/' });
await client.echo.query({ message: 'Hello world!' });如果您从 React 网站调用 API,请考虑使用 Connection 生成器来配置客户端。
有关 tRPC 的更多信息,请参阅 tRPC 文档。
使用 connection 生成器将此项目与工作区中的其他项目集成。以下连接涉及此项目: