跳转到内容

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:

  1. 安装 Nx Console VSCode Plugin 如果您尚未安装
  2. 在VSCode中打开Nx控制台
  3. 点击 Generate (UI) 在"Common Nx Commands"部分
  4. 搜索 @aws/nx-plugin - ts#trpc-api
  5. 填写必需参数
    • 点击 Generate
    参数 类型 默认值 描述
    name 必需 string - API 的名称(必填)。用于生成类名和文件路径。
    computeType string ServerlessApiGatewayRestApi 用于部署此 API 的计算类型。可选择 ServerlessApiGatewayRestApi(默认)或 ServerlessApiGatewayHttpApi。
    integrationPattern string isolated API Gateway 集成的生成方式。可选择 isolated(默认)或 shared。
    auth string IAM 用于对 API 进行身份验证的方法。可选择 IAM(默认)、Cognito 或 None。
    directory string packages 存储应用程序的目录。
    subDirectory string - 项目所在的子目录。默认情况下为项目名称。
    iacProvider string Inherit 首选的 IaC 提供商。默认情况下,这继承自您的初始选择。

    生成器将在 <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 Lambda Powertools 日志记录的中间件
        • tracer.ts 配置 AWS Lambda Powertools 追踪的中间件
        • metrics.ts 配置 AWS Lambda Powertools 指标的中间件
      • local-server.ts 本地开发服务器使用的 tRPC 独立适配器入口点
      • 文件夹client
        • index.ts 用于机器间 API 调用的类型安全客户端
    • tsconfig.json TypeScript 配置
    • project.json 项目配置和构建目标

    由于该生成器会根据您选择的 iacProvider 以基础设施即代码的形式输出,它将在 packages/common 目录下创建一个包含相关 CDK 构造体或 Terraform 模块的项目。

    通用的基础设施即代码项目结构如下:

    • 文件夹packages/common/constructs
      • 文件夹src
        • 文件夹app/ 针对特定项目/生成器的基础设施构造体
        • 文件夹core/ app 目录构造体重用的通用构造体
        • index.ts 导出 app 目录构造体的入口文件
      • project.json 项目构建目标与配置

    部署 API 时会生成以下文件:

    • 文件夹packages/common/constructs/src
      • 文件夹app
        • 文件夹apis
          • <project-name>.ts 用于部署 API 的 CDK 构造
      • 文件夹core
        • 文件夹api
          • http-api.ts 部署 HTTP API 的 CDK 构造(如果你选择部署 HTTP API)
          • rest-api.ts 部署 REST API 的 CDK 构造(如果你选择部署 REST API)
          • utils.ts API 构造的实用工具

    从高层次来看,tRPC API 由将请求委托给特定过程的路由器组成。每个过程都有使用 Zod 模式定义的输入和输出。

    src/schema 目录包含客户端和服务器代码共享的类型。在本包中,这些类型使用 Zod(一个 TypeScript 优先的模式声明和验证库)定义。

    示例模式可能如下所示:

    import { z } from 'zod';
    // 模式定义
    export const UserSchema = z.object({
    name: z.string(),
    height: z.number(),
    dateOfBirth: z.string().datetime(),
    });
    // 对应的 TypeScript 类型
    export type User = z.TypeOf<typeof UserSchema>;

    根据上述模式,User 类型等效于以下 TypeScript:

    interface User {
    name: string;
    height: number;
    dateOfBirth: string;
    }

    模式在服务器和客户端代码之间共享,当需要修改 API 使用的结构时,只需在此处更新即可。

    tRPC API 在运行时自动验证模式,无需在后端手工编写验证逻辑。

    Zod 提供了强大的工具来组合或派生模式,如 .merge.pick.omit 等。更多信息请参阅 Zod 文档网站

    您的 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) => ({ result: opts.input.message }));

    分解上述代码:

    • publicProcedure 定义 API 的公共方法,包含在 src/middleware 中设置的中间件。该中间件包括用于日志记录、追踪和指标的 AWS Lambda Powertools 集成
    • input 接受定义操作预期输入的 Zod 模式。发送到此操作的请求会自动根据此模式验证
    • output 接受定义操作预期输出的 Zod 模式。如果返回不符合模式的输出,实现中会出现类型错误
    • query 接受定义 API 实现的函数。该实现接收包含操作输入的 opts,以及中间件设置的上下文(通过 opts.ctx 访问)。传递给 query 的函数必须返回符合 output 模式的输出

    使用 query 定义实现表示该操作是非变更性的。使用此方法来定义数据检索方法。要实现变更性操作,请改用 mutation 方法。

    如果添加新过程,请确保在 src/router.ts 的路由器中注册它。

    tRPC 订阅允许您使用服务器发送事件(SSE)从服务器向客户端流式传输数据。当您选择 ServerlessApiGatewayRestApi 作为计算类型时,生成器会自动配置流式传输所需的基础设施,以及流式传输 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) {
    // 在数据可用时向客户端产出数据
    for (const chunk of await getResults(opts.input.query)) {
    yield { text: chunk };
    }
    });

    像注册任何其他过程一样在路由器中注册订阅:

    export const appRouter = router({
    echo,
    myStream,
    });

    生成的基础设施对所有 REST API 操作使用具有 ResponseTransferMode.STREAM 的流式传输 Lambda 处理程序,这使订阅能够与常规查询和变更一起工作。

    在实现中,您可以通过抛出 TRPCError 向客户端返回错误响应。这些错误接受表示错误类型的 code,例如:

    throw new TRPCError({
    code: 'NOT_FOUND',
    message: '找不到请求的资源',
    });

    随着 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('操作调用输入', opts.input);
    return ...;
    });

    有关日志记录器的更多信息,请参阅 AWS Lambda Powertools 日志记录器文档

    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 指标文档

    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');
    // ... 要捕获的算法逻辑
    subSegment.close();
    return ...;
    });

    更多信息请参阅 AWS Lambda Powertools 追踪器文档

    您可以通过实现中间件向过程提供的上下文中添加额外值。

    例如,让我们在 src/middleware/identity.ts 中实现一些中间件来从 API 中提取调用用户的详细信息。

    认证假设

    此示例假设 auth 设置为 IAM。对于 Cognito 身份验证,身份中间件更简单,直接从 event 中提取相关声明。

    首先,我们定义要添加到上下文中的内容:

    export interface IIdentityContext {
    identity?: {
    sub: string;
    username: string;
    };
    }

    注意,我们向上下文定义了一个额外的 可选 属性。tRPC 会管理确保在正确配置此中间件的过程中有此定义。

    接下来,我们将实现中间件本身。其结构如下:

    export const createIdentityPlugin = () => {
    const t = initTRPC.context<...>().create();
    return t.procedure.use(async (opts) => {
    // 在此添加过程前运行的逻辑
    const response = await opts.next(...);
    // 在此添加过程后运行的逻辑
    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: `无法确定调用用户`,
    });
    }
    const { Users } = await cognito.listUsers({
    // 假设用户池 ID 在 Lambda 环境中配置
    UserPoolId: process.env.USER_POOL_ID!,
    Limit: 1,
    Filter: `sub="${sub}"`,
    });
    if (!Users || Users.length !== 1) {
    throw new TRPCError({
    code: 'FORBIDDEN',
    message: `找不到主题 ID 为 ${sub} 的用户`,
    });
    }
    // 向其他过程提供身份信息
    return await opts.next({
    ctx: {
    ...opts.ctx,
    identity: {
    sub,
    username: Users[0].Username!,
    },
    },
    });
    });
    };

    tRPC API 生成器根据您选择的 iacProvider 创建 CDK 或 Terraform 基础设施即代码。您可以使用此代码部署 tRPC API。

    common/constructs 文件夹中包含用于部署 API 的 CDK 构造。您可以在 CDK 应用程序中使用此构造,例如:

    import { MyApi } from ':my-scope/common-constructs`;
    export class ExampleStack extends Stack {
    constructor(scope: Construct, id: string) {
    // 将 API 添加到堆栈
    const api = new MyApi(this, 'MyApi', {
    integrations: MyApi.defaultIntegrations(this).build(),
    });
    }
    }

    这将设置您的 API 基础设施,包括 AWS API Gateway REST 或 HTTP API、用于业务逻辑的 AWS Lambda 函数,以及根据您选择的 auth 方法配置的身份验证。

    REST/HTTP API 的 CDK 构造被配置为提供类型安全接口,用于为每个操作定义集成。

    CDK 构造提供如下所述完整的类型安全集成支持。

    您可以使用静态方法 defaultIntegrations 来应用默认模式,这会为每个操作定义独立的 AWS Lambda 函数:

    new MyApi(this, 'MyApi', {
    integrations: MyApi.defaultIntegrations(this).build(),
    });

    您可以通过 API 构造的 integrations 属性以类型安全的方式访问底层 AWS Lambda 函数。例如,如果您的 API 定义了名为 sayHello 的操作,并需要为此函数添加权限:

    const api = new MyApi(this, 'MyApi', {
    integrations: MyApi.defaultIntegrations(this).build(),
    });
    // sayHello 的类型与 API 中定义的操作相匹配
    api.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');

    如需自定义创建 Lambda 函数时的默认选项,可使用 withDefaultOptions 方法。例如为所有 Lambda 函数配置 VPC:

    const vpc = new Vpc(this, 'Vpc', ...);
    new MyApi(this, 'MyApi', {
    integrations: MyApi.defaultIntegrations(this)
    .withDefaultOptions({
    vpc,
    })
    .build(),
    });

    使用 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(),
    });
    // 后续可类型安全地访问我们定义的 bucket 属性
    api.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(...) // REST API 使用,HTTP API 使用 HttpUserPoolAuthorizer
    }
    },
    })
    .build(),
    });

    您可以选择不使用默认集成,直接为每个操作提供集成。这在需要不同集成类型或希望在添加新操作时收到类型错误提示时非常有用:

    new MyApi(this, 'MyApi', {
    integrations: {
    sayHello: {
    integration: new LambdaIntegration(...),
    },
    getDocumentation: {
    integration: new HttpIntegration(...),
    },
    },
    });

    生成的 CDK API 构造支持两种集成模式:

    • isolated 为每个操作创建一个 Lambda 函数。这是生成 API 的默认模式。
    • shared 创建单个默认路由 Lambda 函数并在所有操作中复用,除非您覆盖特定集成。

    isolated 为每个操作提供更细粒度的权限和配置。shared 减少 Lambda 和 API Gateway 集成的数量,同时仍允许选择性覆盖。

    例如,将 pattern 设置为 'shared' 会创建单个函数而不是为每个集成创建一个:

    packages/common/constructs/src/app/apis/my-api.ts
    export class MyApi<...> extends ... {
    public static defaultIntegrations = (scope: Construct) => {
    ...
    return IntegrationBuilder.rest({
    pattern: 'shared',
    ...
    });
    };
    }

    如果选择使用 IAM 身份验证,可以授予 API 访问权限:

    api.grantInvokeAccess(myIdentityPool.authenticatedRole);

    生成器会自动配置一个使用 Rolldown 创建部署包的 bundle 目标:

    Terminal window
    pnpm nx bundle <project-name>

    Rolldown 配置位于 rolldown.config.ts 文件中,每个要生成的包都有对应的入口配置。如果定义了多个包,Rolldown 会并行管理这些包的创建过程。

    您可以使用 serve 目标运行 API 的本地服务器,例如:

    Terminal window
    pnpm nx serve my-api

    本地服务器的入口点是 src/local-server.ts

    当对 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,请考虑使用 连接 生成器来配置客户端。

    有关 tRPC 的更多信息,请参阅 tRPC 文档