Skip to content

tRPC

tRPC はエンドツーエンドの型安全性を備えたTypeScript API構築フレームワークです。tRPCを使用すると、API操作の入力と出力の更新が即座にクライアントコードに反映され、プロジェクトの再ビルドなしにIDE上で変更を確認できます。

tRPC APIジェネレータは、AWS CDKまたはTerraformのインフラストラクチャ設定を含む新しいtRPC APIを作成します。生成されるバックエンドはサーバーレスデプロイ用にAWS Lambdaを使用し、AWS API Gateway APIを介して公開され、Zodを使用したスキーマ検証が含まれます。また、ロギング、AWS X-Rayトレーシング、Cloudwatchメトリクスを含むオブザーバビリティのためにAWS Lambda Powertoolsを設定します。

新しいtRPC APIは2つの方法で生成できます:

  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>ディレクトリに以下のプロジェクト構造を作成します:

    • Directorysrc
      • init.ts バックエンドtRPC初期化
      • router.ts tRPCルータ定義(LambdaハンドラAPIエントリーポイント)
      • Directoryschema Zodを使用したスキーマ定義
        • echo.ts 「echo」プロシージャの入力・出力例定義
      • Directoryprocedures APIが公開するプロシージャ(操作)
        • echo.ts サンプルプロシージャ
      • Directorymiddleware
        • error.ts エラーハンドリング用ミドルウェア
        • logger.ts AWS Powertools for Lambdaロギング設定ミドルウェア
        • tracer.ts AWS Powertools for Lambdaトレーシング設定ミドルウェア
        • metrics.ts AWS Powertools for Lambdaメトリクス設定ミドルウェア
      • local-server.ts ローカル開発サーバー用tRPCスタンドアロンアダプタエントリーポイント
      • Directoryclient
        • index.ts マシン間API呼び出し用型安全クライアント
    • tsconfig.json TypeScript設定
    • project.json プロジェクト設定とビルドターゲット

    このジェネレータは選択した iacProvider に基づいてInfrastructure as Codeを生成するため、packages/common に関連するCDKコンストラクトまたはTerraformモジュールを含むプロジェクトを作成します。

    共通のInfrastructure as Codeプロジェクトは以下の構造を持ちます:

    • Directorypackages/common/constructs
      • Directorysrc
        • Directoryapp/ プロジェクト/ジェネレータ固有のインフラストラクチャ用コンストラクト
        • Directorycore/ app 内のコンストラクトで再利用される汎用コンストラクト
        • index.ts app からコンストラクトをエクスポートするエントリーポイント
      • project.json プロジェクトのビルドターゲットと設定

    APIをデプロイするために、以下のファイルが生成されます:

    • Directorypackages/common/constructs/src
      • Directoryapp
        • Directoryapis
          • <project-name>.ts APIをデプロイするためのCDKコンストラクト
      • Directorycore
        • Directoryapi
          • http-api.ts HTTP APIをデプロイするCDKコンストラクト(HTTP APIをデプロイする選択をした場合)
          • rest-api.ts REST APIをデプロイするCDKコンストラクト(REST APIをデプロイする選択をした場合)
          • utils.ts APIコンストラクト用のユーティリティ

    大まかに言うと、tRPC APIはリクエストを特定のプロシージャに委譲するルータで構成されます。各プロシージャはZodスキーマで定義された入力と出力を持ちます。

    src/schemaディレクトリにはクライアントとサーバーコード間で共有される型が含まれます。これらはTypeScriptファーストのスキーマ宣言・検証ライブラリであるZodを使用して定義されます。

    サンプルスキーマの例:

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

    上記の構成要素:

    • publicProceduresrc/middlewareに設定されたミドルウェアを含むAPIの公開メソッドを定義
    • input:操作の期待される入力を定義するZodスキーマを受け入れる
    • output:操作の期待される出力を定義するZodスキーマを受け入れる
    • query:APIの実装を定義する関数を受け入れる。optsには入力値とopts.ctx経由でミドルウェアが設定したコンテキストが含まれる

    queryの使用は操作が非変更的であることを示します。データ取得用メソッドに使用します。変更操作にはmutationメソッドを使用します。

    新しいプロシージャを追加する場合は、src/router.tsでルータに登録してください。

    実装では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,
    }),
    ...
    })

    クライアントはこのグループ化された操作を受け取り、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 Loggerドキュメントを参照。

    AWS Lambda Powertoolsメトリクスはsrc/middleware/metrics.tsで設定され、opts.ctx.metrics経由でアクセス可能:

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

    詳細はAWS Lambda Powertools Metricsドキュメントを参照。

    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('MyAlgorithm');
    // ... トレース対象のアルゴリズムロジック
    subSegment.close();
    return ...;
    });

    詳細はAWS Lambda Powertools Tracerドキュメントを参照。

    コンテキストに追加の値を提供するミドルウェアを実装できます。例として、APIの呼び出し元ユーザー詳細を抽出するミドルウェア:

    この例はauthIAMに設定されている場合を想定。Cognito認証の場合、eventからクレームを抽出可能

    コンテキストインターフェース定義:

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

    ミドルウェア実装(REST 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 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;
    // ...(ユーザー情報取得ロジック)
    return await opts.next({
    ctx: {
    ...opts.ctx,
    identity: {
    sub,
    username: Users[0].Username!,
    },
    },
    });
    });
    };

    tRPC APIジェネレータは選択したiacProviderに基づきCDKまたはTerraformのインフラストラクチャコードを作成します。

    common/constructsフォルダの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(),
    });
    }
    }

    Cognito認証を使用する場合:

    import { MyApi, UserIdentity } from ':my-scope/common-constructs';
    export class ExampleStack extends Stack {
    constructor(scope: Construct, id: string) {
    const identity = new UserIdentity(this, 'Identity');
    const api = new MyApi(this, 'MyApi', {
    identity,
    });
    }
    }

    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: [...],
    }));

    デフォルトオプションのカスタマイズ

    Section titled “デフォルトオプションのカスタマイズ”

    デフォルト統合で作成されるLambda関数のオプションをカスタマイズするには、withDefaultOptionsメソッドを使用します。例えば、すべてのLambda関数をVPC内に配置する場合:

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

    withOverridesメソッドを使用して特定のオペレーションの統合をオーバーライドできます。各オーバーライドは、HTTPまたはREST APIに適したCDK統合コンストラクトに型付けされた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(...);

    オーソライザーのオーバーライド

    Section titled “オーソライザーのオーバーライド”

    統合に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用、HTTP APIの場合はHttpUserPoolAuthorizer
    }
    },
    })
    .build(),
    });

    必要に応じて、デフォルト統合を使用せずに各オペレーションに直接統合を指定できます。例えば、オペレーションごとに異なる統合タイプを使用する場合や、新しいオペレーション追加時に型エラーを受け取りたい場合に有用です:

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

    すべてのAPIリクエストを処理する単一のLambda関数をデプロイしたい場合、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 {
    // すべての統合で同じルータLambdaハンドラを参照
    integration: new LambdaIntegration(router),
    };
    },
    });
    };
    }

    必要に応じて、router関数をdefaultIntegrationsのパラメータとして定義するなど、他の方法でコードを修正することもできます。

    api.grantInvokeAccess(myIdentityPool.authenticatedRole);

    serveターゲットでローカルサーバーを起動:

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

    型安全なクライアントを作成:

    import { createMyApiClient } from ':my-scope/my-api';
    const client = createMyApiClient({ url: 'https://my-api-url.example.com/' });
    await client.echo.query({ message: 'Hello world!' });

    tRPC公式ドキュメントを参照してください。