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の生成
Section titled “tRPC APIの生成”新しいtRPC APIは2つの方法で生成できます:
- インストール Nx Console VSCode Plugin まだインストールしていない場合
- VSCodeでNxコンソールを開く
- クリック
Generate (UI)
"Common Nx Commands"セクションで - 検索
@aws/nx-plugin - ts#trpc-api
- 必須パラメータを入力
- クリック
Generate
pnpm nx g @aws/nx-plugin:ts#trpc-api
yarn nx g @aws/nx-plugin:ts#trpc-api
npx nx g @aws/nx-plugin:ts#trpc-api
bunx nx g @aws/nx-plugin:ts#trpc-api
パラメータ | 型 | デフォルト | 説明 |
---|---|---|---|
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. |
ジェネレータの出力
Section titled “ジェネレータの出力”ジェネレータは<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の実装
Section titled “tRPC 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ドキュメントを参照してください。
ルーターとプロシージャ
Section titled “ルーターとプロシージャ”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の公開メソッドを定義input
は操作の期待入力を定義するZodスキーマを受け入れ、自動検証output
は操作の期待出力を定義するZodスキーマを受け入れ、スキーマに準拠しない出力の場合型エラーが発生query
はAPIの実装を定義する関数を受け入れ、opts
入力とopts.ctx
のコンテキストにアクセス可能
query
の使用は非変異操作を示します。データ変更操作にはmutation
メソッドを使用します。新しいプロシージャを追加する場合は、src/router.ts
のルーターに登録する必要があります。
tRPC APIのカスタマイズ
Section titled “tRPC APIのカスタマイズ”実装では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ドキュメントを参照。
メトリクス記録
Section titled “メトリクス記録”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ドキュメントを参照。
X-Rayトレーシングの微調整
Section titled “X-Rayトレーシングの微調整”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ドキュメントを参照。
カスタムミドルウェアの実装
Section titled “カスタムミドルウェアの実装”コンテキストに追加値を提供するミドルウェアを実装できます。例としてsrc/middleware/identity.ts
にAPI呼び出し元の詳細を抽出するミドルウェアを作成:
この例はauth
がIAM
に設定されていることを想定。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!, }, }, }); });};
import { CognitoIdentityProvider } from '@aws-sdk/client-cognito-identity-provider';import { initTRPC, TRPCError } from '@trpc/server';import { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda';import { APIGatewayProxyEventV2WithIAMAuthorizer } from 'aws-lambda';
// ... インターフェース定義 ...
export const createIdentityPlugin = () => { const t = initTRPC.context<IIdentityContext & CreateAWSLambdaContextOptions<APIGatewayProxyEventV2WithIAMAuthorizer>>().create();
const cognito = new CognitoIdentityProvider();
return t.procedure.use(async (opts) => { // ... HTTP API用の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のデプロイ
Section titled “tRPC APIのデプロイ”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インフラを構築します。
型安全な統合
Section titled “型安全な統合”REST/HTTP API CDKコンストラクトは、各操作の統合を定義するための型安全なインターフェースを提供するように設定されています。
デフォルト統合
Section titled “デフォルト統合”静的メソッドdefaultIntegrations
を使用して、各操作ごとに個別のAWS Lambda関数を定義するデフォルトパターンを利用できます:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
統合へのアクセス
Section titled “統合へのアクセス”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(),});
統合のオーバーライド
Section titled “統合のオーバーライド”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(),});
明示的な統合
Section titled “明示的な統合”デフォルト統合を使用せず、各操作に直接統合を指定することも可能です。これは例えば、操作ごとに異なるタイプの統合を使用する必要がある場合や、新しい操作を追加した際に型エラーを受け取りたい場合に有用です:
new MyApi(this, 'MyApi', { integrations: { sayHello: { integration: new LambdaIntegration(...), }, getDocumentation: { integration: new HttpIntegration(...), }, },});
ルーターパターン
Section titled “ルーターパターン”すべてのAPIリクエストを処理する単一のLambda関数をデプロイする場合は、defaultIntegrations
メソッドを編集して統合ごとではなく単一の関数を作成できます:
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);
ローカルtRPCサーバー
Section titled “ローカルtRPCサーバー”serve
ターゲットでローカルサーバーを起動:
pnpm nx run @my-scope/my-api:serve
yarn nx run @my-scope/my-api:serve
npx nx run @my-scope/my-api:serve
bunx nx run @my-scope/my-api:serve
エントリーポイントはsrc/local-server.ts
です。API変更時に自動リロードされます。
tRPC APIの呼び出し
Section titled “tRPC 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ドキュメントを参照してください。