跳转到内容

tRPC

tRPC 是一个用于构建端到端类型安全 TypeScript API 的框架。使用 tRPC 时,API 操作输入输出的变更会立即反映在客户端代码中,并直接在 IDE 中可见,无需重新构建项目。

tRPC API 生成器会创建一个新的 tRPC API 并配置 AWS CDK 基础设施。生成的后端使用 AWS Lambda 进行无服务器部署,并通过 Zod 实现模式验证。同时集成了 AWS Lambda Powertools 用于可观测性,包括日志记录、AWS X-Ray 追踪和 CloudWatch 指标。

使用方式

生成 tRPC API

可通过两种方式生成新的 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.

    生成器输出

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

    • 文件夹schema
      • 文件夹src
        • index.ts 模式入口文件
        • 文件夹procedures
          • echo.ts 使用 Zod 的 “echo” 过程共享模式定义
      • tsconfig.json TypeScript 配置
      • project.json 项目配置与构建目标
    • 文件夹backend
      • 文件夹src
        • init.ts 后端 tRPC 初始化
        • router.ts tRPC 路由定义(Lambda 处理程序 API 入口)
        • 文件夹procedures 暴露的 API 过程(操作)
          • echo.ts 示例过程
        • 文件夹middleware
          • error.ts 错误处理中间件
          • logger.ts 配置 AWS Lambda 日志记录的中间件
          • tracer.ts 配置 AWS Lambda 追踪的中间件
          • metrics.ts 配置 AWS Lambda 指标的中间件
        • local-server.ts 本地开发服务器的独立适配器入口
        • 文件夹client
          • index.ts 机器间调用的类型安全客户端
      • tsconfig.json TypeScript 配置
      • project.json 项目配置与构建目标

    生成器还会在 packages/common/constructs 目录下创建用于部署 API 的 CDK 构造。

    实现 tRPC API

    如上所示,tRPC API 包含两个主要组件:schemabackend,在工作区中定义为独立包。

    模式(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 文档

    后端(Backend)

    backend 目录包含 API 实现,用于定义 API 操作及其输入、输出和实现。

    API 入口位于 src/router.ts,该文件包含根据操作调用将请求路由到 “procedures” 的 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 接受定义操作实现的函数。该实现接收 opts 参数,包含操作输入及中间件设置的上下文(通过 opts.ctx 访问)。函数必须返回符合 output 模式的输出。

    使用 query 表示该操作非变更性,适用于数据检索。对于变更性操作,应使用 mutation 方法。

    新增操作时,需在 src/router.ts 的路由中注册。

    定制 tRPC API

    错误处理

    在实现中,可通过抛出 TRPCError 返回错误响应。例如:

    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,
    }),
    ...
    })

    客户端调用分组操作示例如下:

    client.users.list.query();

    日志记录

    AWS Lambda Powertools 日志记录器配置于 src/middleware/logger.ts,可通过 opts.ctx.logger 访问。例如:

    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 访问。例如:

    export const echo = publicProcedure
    .input(...)
    .output(...)
    .query(async (opts) => {
    opts.ctx.metrics.addMetric('调用次数', 'Count', 1);
    return ...;
    });

    更多信息请参考 AWS Lambda Powertools 指标文档

    X-Ray 追踪调优

    AWS Lambda Powertools 追踪器配置于 src/middleware/tracer.ts,可通过 opts.ctx.tracer 访问。例如:

    export const echo = publicProcedure
    .input(...)
    .output(...)
    .query(async (opts) => {
    const subSegment = opts.ctx.tracer.getSegment()!.addNewSubsegment('我的算法');
    // ... 需捕获的算法逻辑
    subSegment.close();
    return ...;
    });

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

    自定义中间件

    通过实现中间件,可为过程上下文添加额外值。例如在 src/middleware/identity.ts 中实现身份中间件:

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

    首先定义上下文扩展:

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

    接着实现中间件:

    // REST API 实现示例

    部署 tRPC API

    生成器在 common/constructs 目录创建 CDK 构造。在 CDK 应用中使用示例:

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

    此配置包括 AWS API Gateway REST/HTTP API、业务逻辑的 Lambda 函数及基于所选 auth 方法的认证。

    类型安全集成

    REST/HTTP API 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 属性(对应 HTTP 或 REST API 的 CDK 集成构造)。该方法同样具有类型安全性。例如,若要将 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 函数处理所有 API 请求,可以修改 API 的 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({
    ...
    defaultIntegrationOptions: {},
    buildDefaultIntegration: (op) => {
    return {
    // 所有集成引用同一个路由函数
    integration: new LambdaIntegration(router),
    };
    },
    });
    };
    }

    您也可以采用其他自定义方式,例如将 router 函数作为 defaultIntegrations 的参数而非在方法内构造。

    授权访问(仅限 IAM)

    若使用 IAM 认证,可通过 grantInvokeAccess 方法授权 API 访问。例如授权 Cognito 认证用户:

    api.grantInvokeAccess(myIdentityPool.authenticatedRole);

    本地 tRPC 服务器

    使用 serve 目标运行本地服务器:

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

    本地服务器入口为 src/local-server.ts

    调用 tRPC API

    可创建类型安全客户端调用 API。后端间调用示例:

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