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 プロジェクト設定とビルドターゲット
また、ジェネレータはAPIのデプロイに使用できるCDKコンストラクトをpackages/common/constructs
ディレクトリに作成します。
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の公開メソッドを定義します。このミドルウェアにはロギング、トレーシング、メトリクスのためのAWS Lambda Powertools統合が含まれますinput
は操作の期待される入力を定義するZodスキーマを受け入れます。この操作に送信されるリクエストは自動的にこのスキーマに対して検証されますoutput
は操作の期待される出力を定義するZodスキーマを受け入れます。スキーマに準拠しない出力を返す場合、実装で型エラーが発生しますquery
はAPIの実装を定義する関数を受け入れます。この実装はopts
を受け取り、操作に渡されたinput
と、opts.ctx
で利用可能なミドルウェアによって設定されたその他のコンテキストを含みます。query
に渡される関数はoutput
スキーマに準拠する出力を返す必要があります
query
の使用は操作が非変異的であることを示します。データ取得メソッドの定義に使用します。変異的操作を実装する場合は、代わりにmutation
メソッドを使用してください。
新しいプロシージャを追加する場合は、src/router.ts
のルーターに登録する必要があります。
tRPC APIのカスタマイズ
Section titled “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ドキュメントを参照してください。
メトリクスの記録
Section titled “メトリクスの記録”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トレーシングの微調整
Section titled “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ドキュメントを参照してください。
カスタムミドルウェアの実装
Section titled “カスタムミドルウェアの実装”ミドルウェアを実装することで、プロシージャに提供されるコンテキストに追加の値を追加できます。
例として、APIから呼び出しユーザーの詳細を抽出するミドルウェアをsrc/middleware/identity.ts
に実装します。
この例はauth
がIAM
に設定されていることを前提としています。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!, }, }, }); });};
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({ // 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のデプロイ
Section titled “tRPC APIのデプロイ”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インフラが設定されます。
型安全な統合
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へのアクセスを許可できます。例: 認証済みCognitoユーザーにAPIアクセスを許可:
api.grantInvokeAccess(myIdentityPool.authenticatedRole);
ローカルtRPCサーバー
Section titled “ローカル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
です。
APIに変更を加えると自動的にリロードされます。
tRPC APIの呼び出し
Section titled “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 Connectionジェネレータの使用を検討してください。
tRPCの詳細についてはtRPC公式ドキュメントを参照してください。