Skip to content

tRPC

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

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

使用方法

tRPC API の生成

新しい 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.

    ジェネレータの出力

    ジェネレータは <directory>/<api-name> ディレクトリに以下のプロジェクト構造を作成します:

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

    ジェネレータはまた、API のデプロイに使用できる CDK コンストラクトを packages/common/constructs ディレクトリに作成します。

    tRPC API の実装

    上記のように、tRPC API にはワークスペース内で個別のパッケージとして定義される schemabackend の2つの主要コンポーネントがあります。

    スキーマ

    スキーマパッケージはクライアントとサーバーコード間で共有される型を定義します。これらの型は 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 ドキュメント を参照してください。

    バックエンド

    backend フォルダには API の実装が含まれ、API 操作とその入力、出力、実装を定義します。

    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 を受け取り、操作に渡された inputopts.ctx で利用可能なミドルウェアによって設定されたコンテキストを含みます。query に渡される関数は output スキーマに準拠する出力を返す必要があります

    query の使用は操作が非変異的であることを示します。データ取得メソッドの定義に使用します。変異的操作を実装する場合は、代わりに mutation メソッドを使用してください。

    新しい操作を追加する場合は、src/router.ts のルーターに登録してください。

    tRPC API のカスタマイズ

    エラーハンドリング

    実装では、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 で設定され、API 実装で opts.ctx.logger 経由でアクセス可能です。CloudWatch Logs へのロギングや、構造化ログメッセージに含める追加値の制御に使用できます:

    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 経由でアクセス可能です。AWS SDK をインポートせずに CloudWatch にメトリクスを記録するために使用できます:

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

    詳細は AWS Lambda Powertools Metrics ドキュメント を参照してください。

    X-Ray トレーシングの微調整

    AWS Lambda Powertools トレーサーは src/middleware/tracer.ts で設定され、opts.ctx.tracer 経由でアクセス可能です。API リクエストのパフォーマンスとフローの詳細な可視化のために AWS X-Ray トレースを追加するために使用できます:

    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 から呼び出しユーザーの詳細を抽出するミドルウェアを src/middleware/identity.ts に実装します。

    この例は authIAM に設定されていることを想定しています。Cognito 認証の場合、イベントから関連するクレームを抽出する方が簡単です。

    まず、コンテキストに追加する内容を定義します:

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

    次に、ミドルウェアを実装します:

    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({
    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 のデプロイ

    tRPC バックエンドジェネレータは common/constructs フォルダに API デプロイ用の 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メソッドを使用して特定の操作の統合をオーバーライドできます。各オーバーライドは、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(),
    });
    // 後ほど別ファイルで、定義したバケットプロパティに型安全にアクセス可能
    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の場合はCognitoUserPoolsAuthorizer、HTTP APIの場合はHttpUserPoolAuthorizer
    }
    },
    })
    .build(),
    });

    明示的な統合

    デフォルト統合を使用せず、各操作に直接統合を指定することも可能です。これは例えば、操作ごとに異なるタイプの統合を使用する必要がある場合や、新しい操作を追加した際に型エラーを受け取りたい場合に有用です:

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

    ルーターパターン

    すべてのAPIリクエストを処理する単一の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({
    ...
    defaultIntegrationOptions: {},
    buildDefaultIntegration: (op) => {
    return {
    // すべての統合で同じルーターLambdaハンドラーを参照
    integration: new LambdaIntegration(router),
    };
    },
    });
    };
    }

    必要に応じて、router関数をメソッド内で構築する代わりにdefaultIntegrationsのパラメータとして定義するなど、コードを変更することも可能です。

    アクセス権限付与(IAM のみ)

    IAM 認証を選択した場合、grantInvokeAccess メソッドで API へのアクセスを許可できます:

    api.grantInvokeAccess(myIdentityPool.authenticatedRole);

    ローカル tRPC サーバー

    serve ターゲットを使用して API のローカルサーバーを実行できます:

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

    ローカルサーバーのエントリポイントは src/local-server.ts です。

    tRPC API の呼び出し

    型安全な方法で 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 ドキュメント を参照してください。