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

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

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

    クライアントはこのグループ化された操作を呼び出せます:

    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ドキュメントを参照。

    コンテキストに追加値を提供するミドルウェアを実装できます。例としてsrc/middleware/identity.tsに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) => {
    // ... 認証プロバイダーからのsub抽出ロジック ...
    // Cognitoユーザー検索
    const { Users } = await cognito.listUsers({
    UserPoolId: process.env.USER_POOL_ID!,
    Limit: 1,
    Filter: `sub="${sub}"`,
    });
    // コンテキストにidentityを追加
    return await opts.next({
    ctx: {
    ...opts.ctx,
    identity: {
    sub,
    username: Users[0].Username!,
    },
    },
    });
    });
    };

    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、Lambda関数、認証方法を含むAPIインフラを構築します。

    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認証時)

    Section titled “アクセス権限付与(IAM認証時)”

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

    api.grantInvokeAccess(myIdentityPool.authenticatedRole);

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

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

    エントリーポイントはsrc/local-server.tsです。API変更時に自動リロードされます。

    型安全なtRPCクライアントを作成可能。バックエンド間呼び出し例:

    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 Connectionジェネレータの使用を検討してください。

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