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

    • 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 プロジェクト設定とビルドターゲット

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

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

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

    スキーマの例:

    import { z } from 'zod/v4';
    // スキーマ定義
    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は、src/middlewareに設定されたミドルウェアを含むAPIの公開メソッドを定義。このミドルウェアにはロギング、トレーシング、メトリクスのための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で設定され、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ドキュメントを参照してください。

    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ジェネレータは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: [...],
    }));

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

    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(),
    });
    // 後ほど別ファイルで、定義したバケットプロパティに型安全にアクセス可能
    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 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認証を選択した場合、grantInvokeAccessメソッドでAPIアクセスを許可できます:

    api.grantInvokeAccess(myIdentityPool.authenticatedRole);

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

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

    ローカルサーバーのエントリーポイントはsrc/local-server.tsです。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 Connectionジェネレータを使用してクライアントを設定してください。

    tRPCの詳細については、tRPCドキュメントを参照してください。