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つの方法で生成できます:
- インストール 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. |
ジェネレータの出力
ジェネレータは <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 にはワークスペース内で個別のパッケージとして定義される schema
と backend
の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
を受け取り、操作に渡されたinput
とopts.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
に実装します。
この例は auth
が IAM
に設定されていることを想定しています。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!, }, }, }); });};
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 interface IIdentityContext { identity?: { sub: string; username: string; };}
export const createIdentityPlugin = () => { const t = initTRPC.context<IIdentityContext & CreateAWSLambdaContextOptions<APIGatewayProxyEventV2WithIAMAuthorizer>>().create();
const cognito = new CognitoIdentityProvider();
return t.procedure.use(async (opts) => { const cognitoIdentity = opts.ctx.event.requestContext?.authorizer?.iam ?.cognitoIdentity as unknown as | { amr: string[]; } | undefined;
const sub = (cognitoIdentity?.amr ?? []) .flatMap((s) => (s.includes(':CognitoSignIn:') ? [s] : [])) .map((s) => { const parts = s.split(':'); return parts[parts.length - 1]; })?.[0];
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
メソッドを編集して統合ごとではなく単一の関数を作成できます:
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 のローカルサーバーを実行できます:
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
です。
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 ドキュメント を参照してください。