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';
    // スキーマ定義
    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を受け取り、操作に渡されたinputと、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認証の場合、eventから関連するクレームを抽出する方が簡単です。

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

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

    この追加の_オプショナル_プロパティが正しく設定されたミドルウェアを持つプロシージャで確実に定義されるよう、tRPCが管理します。

    次に、ミドルウェア本体を実装。構造は以下の通り:

    export const createIdentityPlugin = () => {
    const t = initTRPC.context<...>().create();
    return t.procedure.use(async (opts) => {
    // プロシージャ実行前のロジック
    const response = await opts.next(...);
    // プロシージャ実行後のロジック
    return response;
    });
    };

    この例では、API Gatewayイベントから呼び出し元のCognitoユーザーの詳細(subject ID「sub」)を抽出し、Cognitoからユーザー詳細を取得します。実装はREST APIとHTTP 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 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({
    // Lambda環境にユーザープールIDが設定されていることを前提
    UserPoolId: process.env.USER_POOL_ID!,
    Limit: 1,
    Filter: `sub="${sub}"`,
    });
    if (!Users || Users.length !== 1) {
    throw new TRPCError({
    code: 'FORBIDDEN',
    message: `subjectId ${sub}のユーザーが見つかりませんでした`,
    });
    }
    // コンテキストにidentityを提供
    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) {
    // スタックにAPIを追加
    const api = new MyApi(this, 'MyApi', {
    integrations: MyApi.defaultIntegrations(this).build(),
    });
    }
    }

    これにより、AWS API Gateway REST/HTTP API、ビジネスロジック用AWS Lambda関数、選択したauthメソッドに基づく認証を含む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認証を選択した場合、grantInvokeAccessメソッドを使用してAPIへのアクセスを許可できます。例: 認証済みCognitoユーザーに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公式ドキュメントを参照してください。