跳转到内容

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 - The name of the API (required). Used to generate class names and file paths.
    computeType string ServerlessApiGatewayRestApi The type of compute to use to deploy this API. Choose between ServerlessApiGatewayRestApi (default) or ServerlessApiGatewayHttpApi.
    auth string IAM The method used to authenticate with your API. Choose between IAM (default), Cognito or None.
    directory string packages The directory to store the application in.
    iacProvider string CDK The preferred IaC provider

    生成器将在 <directory>/<api-name> 目录下创建以下项目结构:

    • 文件夹src
      • init.ts 后端 tRPC 初始化
      • router.ts tRPC 路由定义(Lambda 处理程序 API 入口点)
      • 文件夹schema 使用 Zod 的模式定义
        • echo.ts “echo” 过程输入输出的示例定义
      • 文件夹procedures API 暴露的过程(或操作)
        • echo.ts 示例过程
      • 文件夹middleware
        • error.ts 错误处理中间件
        • logger.ts 配置 AWS Powertools Lambda 日志记录的中间件
        • tracer.ts 配置 AWS Powertools Lambda 追踪的中间件
        • metrics.ts 配置 AWS Powertools Lambda 指标的中间件
      • 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 文档网站

    API 的入口点位于 src/router.ts。该文件包含 Lambda 处理程序,根据调用的操作将请求路由到特定”过程”。每个过程定义预期的输入、输出和实现。

    生成的示例路由器包含一个名为 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 的路由器中注册。

    在实现中,可以通过抛出 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 实现中访问。可用于无需导入 AWS SDK 即可在 CloudWatch 中记录指标,例如:

    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;
    });
    };

    在本例中,我们需要从 API Gateway 事件中提取调用 Cognito 用户的 subject 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: `找不到 subjectId 为 ${sub} 的用户`,
    });
    }
    // 向其他过程提供身份上下文
    return await opts.next({
    ctx: {
    ...opts.ctx,
    identity: {
    sub,
    username: Users[0].Username!,
    },
    },
    });
    });
    };

    tRPC API 生成器根据选择的 iacProvider 创建 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) {
    // 将 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: [...],
    }));

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

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

    使用 withOverrides 方法可以覆盖特定操作的集成。每个覆盖必须指定类型正确的 integration 属性。例如将 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(...),
    },
    },
    });

    如需部署单个 Lambda 函数处理所有请求,可修改 defaultIntegrations 方法:

    packages/common/constructs/src/app/apis/my-api.ts
    export class MyApi<...> extends ... {
    public static defaultIntegrations = (scope: Construct) => {
    const router = new Function(scope, 'RouterHandler', { ... });
    return IntegrationBuilder.rest({
    ...
    buildDefaultIntegration: (op) => {
    return {
    // 所有集成引用同一个路由处理器
    integration: new LambdaIntegration(router),
    };
    },
    });
    };
    }

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

    api.grantInvokeAccess(myIdentityPool.authenticatedRole);

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

    Terminal window
    pnpm nx run @my-scope/my-api:serve

    本地服务器的入口点为 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,考虑使用 API 连接生成器来配置客户端。

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