tRPC
tRPC est un framework pour créer des API en TypeScript avec une sécurité de type de bout en bout. Avec tRPC, les mises à jour des entrées et sorties des opérations d’API sont immédiatement reflétées dans le code client et sont visibles dans votre IDE sans avoir besoin de reconstruire votre projet.
Le générateur d’API tRPC crée une nouvelle API tRPC avec une configuration d’infrastructure AWS CDK ou Terraform. Le backend généré utilise AWS Lambda pour un déploiement serverless, exposé via une API AWS API Gateway, et inclut la validation de schéma avec Zod. Il configure AWS Lambda Powertools pour l’observabilité, incluant la journalisation, le traçage AWS X-Ray et les métriques Cloudwatch.
Utilisation
Section intitulée « Utilisation »Générer une API tRPC
Section intitulée « Générer une API tRPC »Vous pouvez générer une nouvelle API tRPC de deux manières :
Exécuter ce générateur@aws/nx-plugin:ts#api
pnpm nx g @aws/nx-plugin:ts#api yarn nx g @aws/nx-plugin:ts#api npx nx g @aws/nx-plugin:ts#api bunx nx g @aws/nx-plugin:ts#api- Installez le Nx Console VSCode Plugin si ce n'est pas déjà fait
- Ouvrez la console Nx dans VSCode
- Cliquez sur
Generate (UI)dans la section "Common Nx Commands" - Recherchez
@aws/nx-plugin - ts#api - Remplissez les paramètres requis
- Cliquez sur
Generate
Composez votre commande9
Requis
nameRequisstringLe nom de l'API (requis). Utilisé pour générer les noms de classes et les chemins de fichiers.
frameworkenumPar défaut:trpcLe framework d'API à utiliser.
trpcsmithyintegrationPatternenumPar défaut:isolatedComment les intégrations API Gateway sont générées pour l'API. Choisissez entre isolated (par défaut) et shared.
isolatedsharedauthenumPar défaut:iamLa méthode utilisée pour s'authentifier auprès de votre API. Choisissez entre iam (par défaut), cognito ou custom.
iamcognitocustomdirectorystringPar défaut:packagesLe répertoire dans lequel stocker l'application.
iacenumPar défaut:inheritLe fournisseur IaC préféré. Par défaut, celui-ci est hérité de votre sélection initiale.
inheritcdkterraforminfraenumPar défaut:rest-lambdaLe type d'infrastructure à utiliser pour déployer cette API.
rest-lambdahttp-lambdanonesubDirectorystringLe sous-répertoire dans lequel le projet est placé. Par défaut, il s'agit du nom du projet.
preferInstallDependenciesbooleanPar défaut:trueIndique s'il faut privilégier l'installation des dépendances après l'exécution du générateur. Définir à false pour différer l'installation lors de l'exécution de plusieurs générateurs en lot (une installation s'exécute quand même si nécessaire pour que les générateurs suivants puissent calculer le graphe de projet Nx) ; installer une seule fois à la fin.
Sortie du générateur
Section intitulée « Sortie du générateur »Le générateur créera la structure de projet suivante dans le répertoire <directory>/<api-name> :
Répertoiresrc
- index.ts Package entrypoint re-exporting the router, context, client and schema
- init.ts Backend tRPC initialisation
- handler.ts Lambda handler entrypoint
- router.ts tRPC router definition
Répertoireschema Schema definitions using Zod
- index.ts Barrel re-exporting every schema
- echo.ts Example definitions for the input and output of the “echo” procedure
- z-async-iterable.ts Zod helper for subscriptions (REST API only)
Répertoireprocedures Procedures (or operations) exposed by your API
- echo.ts Example procedure
Répertoiremiddleware
- index.ts Barrel re-exporting the middleware, and the procedure context type
- error.ts Middleware for error handling
- logger.ts middleware for configuring AWS Powertools for Lambda logging
- tracer.ts middleware for configuring AWS Powertools for Lambda tracing
- metrics.ts middleware for configuring AWS Powertools for Lambda metrics
- local-server.ts tRPC standalone adapter entrypoint for local development server
Répertoireclient
- index.ts Type-safe client for machine-to-machine API calls
- rolldown.config.ts Bundle configuration for the Lambda deployment package
- tsconfig.json TypeScript configuration
- tsconfig.lib.json TypeScript configuration for the library sources
- tsconfig.spec.json TypeScript configuration for the tests
- vitest.config.mts Vitest configuration
- package.json Project manifest defining the project’s package name and dependencies
- project.json Project configuration and build targets
- README.md Project readme
- .gitignore Ignores the project’s build output
Infrastructure
Section intitulée « Infrastructure »Étant donné que ce générateur fournit de l’infrastructure en tant que code basée sur votre iac choisi, il créera un projet dans packages/common qui inclut les constructs CDK ou modules Terraform pertinents.
Le projet d’infrastructure en tant que code commun est structuré comme suit :
Répertoirepackages/common/constructs
Répertoiresrc
Répertoireapp/ Constructs pour l’infrastructure spécifique à un projet/générateur
- …
Répertoirecore/ Constructs génériques qui sont réutilisés par les constructs dans
app- …
- index.ts Entry point exporting constructs from
app
- project.json Project build targets and configuration
Répertoirepackages/common/terraform
Répertoiresrc
Répertoireapp/ Terraform modules for infrastructure specific to a project/generator
- …
Répertoirecore/ Generic modules which are reused by modules in
app- …
- project.json Project build targets and configuration
Pour déployer votre API, les fichiers suivants sont générés :
Répertoirepackages/common/constructs/src
Répertoireapp
Répertoireapis
- <project-name>.ts CDK construct for deploying your API
Répertoirecore
Répertoireapi
- http-api.ts CDK construct for deploying an HTTP API (if you selected to deploy an HTTP API)
- rest-api.ts CDK construct for deploying a REST API (if you selected to deploy a REST API)
- utils.ts Utilities for the API constructs
Répertoirepackages/common/terraform/src
Répertoireapp
Répertoireapis
Répertoire<project-name>
- <project-name>.tf Module for deploying your API
Répertoirecore
Répertoireapi
Répertoirehttp-api
- http-api.tf Module for deploying an HTTP API (if you selected to deploy an HTTP API)
Répertoirerest-api
- rest-api.tf Module for deploying a REST API (if you selected to deploy a REST API)
Architecture
Section intitulée « Architecture »L’application déployée a l’architecture suivante : une API API Gateway devant une fonction Lambda exécutant votre gestionnaire.
Les REST APIs incluent une Web ACL AWS WAFv2 devant l’étape API Gateway avec l’ensemble de règles par défaut géré par AWS activé.
Les HTTP APIs ne prennent pas en charge WAF directement — si vous avez besoin de la protection WAF, choisissez REST API à la place ou placez l’HTTP API derrière une distribution CloudFront.
Implémenter votre API tRPC
Section intitulée « Implémenter votre API tRPC »À un niveau élevé, les API tRPC consistent en un routeur qui délègue les requêtes à des procédures spécifiques. Chaque procédure a une entrée et une sortie, définies comme un schéma Zod.
Le répertoire src/schema contient les types qui sont partagés entre votre code client et serveur. Dans ce package, ces types sont définis en utilisant Zod, une bibliothèque de déclaration et de validation de schéma TypeScript-first.
Un exemple de schéma pourrait ressembler à ceci :
import { z } from 'zod';
// Schema definitionexport const UserSchema = z.object({ name: z.string(), height: z.number(), dateOfBirth: z.string().datetime(),});
// Corresponding TypeScript typeexport type User = z.TypeOf<typeof UserSchema>;Étant donné le schéma ci-dessus, le type User est équivalent au TypeScript suivant :
interface User { name: string; height: number; dateOfBirth: string;}Les schémas sont partagés par le code serveur et client, fournissant un seul endroit à mettre à jour lors de modifications des structures utilisées dans votre API.
Les schémas sont automatiquement validés par votre API tRPC au moment de l’exécution, ce qui évite de créer manuellement une logique de validation personnalisée dans votre backend.
Zod fournit des utilitaires puissants pour combiner ou dériver des schémas tels que .merge, .pick, .omit et plus encore. Vous pouvez trouver plus d’informations sur le site de documentation Zod.
Routeur et procédures
Section intitulée « Routeur et procédures »Votre routeur tRPC est défini dans src/router.ts, qui enregistre toutes les procédures. Chaque procédure définit l’entrée, la sortie et l’implémentation attendues. Le point d’entrée du gestionnaire Lambda se trouve dans src/handler.ts, qui transmet les requêtes à votre routeur.
Le routeur d’exemple généré pour vous a une seule opération, appelée echo :
import { echo } from './procedures/echo.js';
export const appRouter = router({ echo,});L’exemple de procédure echo est généré pour vous dans src/procedures/echo.ts :
export const echo = publicProcedure .input(EchoInputSchema) .output(EchoOutputSchema) .query((opts) => ({ message: opts.input.message }));Pour décomposer ce qui précède :
publicProceduredéfinit une méthode publique sur l’API, incluant le middleware configuré danssrc/middleware. Ce middleware inclut l’intégration AWS Lambda Powertools pour la journalisation, le traçage et les métriques.inputaccepte un schéma Zod qui définit l’entrée attendue pour l’opération. Les requêtes envoyées pour cette opération sont automatiquement validées par rapport à ce schéma.outputaccepte un schéma Zod qui définit la sortie attendue pour l’opération. Vous verrez des erreurs de type dans votre implémentation si vous ne retournez pas une sortie conforme au schéma.queryaccepte une fonction qui définit l’implémentation de votre API. Cette implémentation reçoitopts, qui contient l’inputpassé à votre opération, ainsi que d’autres contextes configurés par le middleware, disponibles dansopts.ctx. La fonction passée àquerydoit retourner une sortie conforme au schémaoutput.
L’utilisation de query pour définir l’implémentation indique que l’opération n’est pas mutative. Utilisez ceci pour définir des méthodes de récupération de données. Pour implémenter une opération mutative, utilisez plutôt la méthode mutation.
Si vous ajoutez une nouvelle procédure, assurez-vous de l’enregistrer en l’ajoutant au routeur dans src/router.ts.
Abonnements (Streaming)
Section intitulée « Abonnements (Streaming) »Les abonnements tRPC vous permettent de diffuser des données du serveur au client en utilisant Server-Sent Events (SSE). Lorsque vous sélectionnez rest-lambda comme type de calcul, le générateur configure automatiquement l’infrastructure requise pour le streaming, ainsi qu’un gestionnaire Lambda de streaming et l’assistant de schéma ZodAsyncIterable.
Pour définir une procédure d’abonnement, utilisez la méthode .subscription avec une fonction génératrice asynchrone. Utilisez l’assistant ZodAsyncIterable de src/schema/z-async-iterable.ts pour définir le schéma de sortie :
import { publicProcedure } from '../init.js';import { z } from 'zod';import { ZodAsyncIterable } from '../schema/z-async-iterable.js';
const InputSchema = z.object({ query: z.string() });const ChunkSchema = z.object({ text: z.string() });
export const myStream = publicProcedure .input(InputSchema) .output( ZodAsyncIterable({ yield: ChunkSchema, }), ) .subscription(async function* (opts) { // Yield data to the client as it becomes available for (const chunk of await getResults(opts.input.query)) { yield { text: chunk }; } });Enregistrez l’abonnement dans votre routeur comme n’importe quelle autre procédure :
export const appRouter = router({ echo, myStream,});L’infrastructure générée utilise un gestionnaire Lambda de streaming avec ResponseTransferMode.STREAM dans API Gateway pour toutes les opérations d’API REST, ce qui permet aux abonnements de fonctionner aux côtés des requêtes et mutations régulières.
Personnaliser votre API tRPC
Section intitulée « Personnaliser votre API tRPC »Dans votre implémentation, vous pouvez retourner des réponses d’erreur aux clients en lançant une TRPCError. Celles-ci acceptent un code qui indique le type d’erreur, par exemple :
throw new TRPCError({ code: 'NOT_FOUND', message: 'The requested resource could not be found',});Organiser vos opérations
Section intitulée « Organiser vos opérations »Au fur et à mesure que votre API grandit, vous souhaiterez peut-être regrouper les opérations connexes.
Vous pouvez regrouper les opérations en utilisant des routeurs imbriqués, par exemple :
import { getUser } from './procedures/users/get.js';import { listUsers } from './procedures/users/list.js';
const appRouter = router({ users: router({ get: getUser, list: listUsers, }), ...})Les clients reçoivent alors ce regroupement d’opérations, par exemple l’invocation de l’opération listUsers dans ce cas pourrait ressembler à ceci :
client.users.list.query();Journalisation
Section intitulée « Journalisation »Le logger AWS Lambda Powertools est configuré dans src/middleware/logger.ts, et peut être accédé dans une implémentation d’API via opts.ctx.logger. Vous pouvez l’utiliser pour journaliser dans CloudWatch Logs, et/ou contrôler des valeurs supplémentaires à inclure dans chaque message de journal structuré. Par exemple :
export const echo = publicProcedure .input(...) .output(...) .query(async (opts) => { opts.ctx.logger.info('Operation called with input', opts.input);
return ...; });Pour plus d’informations sur le logger, veuillez vous référer à la documentation AWS Lambda Powertools Logger.
Enregistrer des métriques
Section intitulée « Enregistrer des métriques »Les métriques AWS Lambda Powertools sont configurées dans src/middleware/metrics.ts, et peuvent être accédées dans une implémentation d’API via opts.ctx.metrics. Vous pouvez l’utiliser pour enregistrer des métriques dans CloudWatch sans avoir besoin d’importer et d’utiliser le SDK AWS, par exemple :
export const echo = publicProcedure .input(...) .output(...) .query(async (opts) => { opts.ctx.metrics.addMetric('Invocations', 'Count', 1);
return ...; });Pour plus d’informations, veuillez vous référer à la documentation AWS Lambda Powertools Metrics.
Affiner le traçage X-Ray
Section intitulée « Affiner le traçage X-Ray »Le traceur AWS Lambda Powertools est configuré dans src/middleware/tracer.ts, et peut être accédé dans une implémentation d’API via opts.ctx.tracer. Vous pouvez l’utiliser pour ajouter des traces avec AWS X-Ray afin de fournir des informations détaillées sur les performances et le flux des requêtes API. Par exemple :
export const echo = publicProcedure .input(...) .output(...) .query(async (opts) => { const subSegment = opts.ctx.tracer.getSegment()!.addNewSubsegment('MyAlgorithm'); // ... my algorithm logic to capture subSegment.close();
return ...; });Pour plus d’informations, veuillez vous référer à la documentation AWS Lambda Powertools Tracer.
Implémenter un middleware personnalisé
Section intitulée « Implémenter un middleware personnalisé »Vous pouvez ajouter des valeurs supplémentaires au contexte fourni aux procédures en implémentant un middleware.
À titre d’exemple, implémentons un middleware pour extraire des détails sur l’utilisateur appelant notre API dans src/middleware/identity.ts.
Cet exemple présente un middleware d’identité pour l’authentification IAM. Nous recherchons l’appelant dans Cognito en utilisant le sub extrait de l’événement API Gateway.
La recherche utilise le client Cognito Identity Provider, qui n’est pas une dépendance d’une API tRPC générée. Installez-le d’abord dans votre projet d’API :
pnpm add @aws-sdk/client-cognito-identity-provider@3.1126.0 --filter my-apiyarn workspace @my-scope/my-api add @aws-sdk/client-cognito-identity-provider@3.1126.0npm install --legacy-peer-deps @aws-sdk/client-cognito-identity-provider@3.1126.0 -w packages/my-apibun add @aws-sdk/client-cognito-identity-provider@3.1126.0 --cwd packages/my-apiTout d’abord, nous définissons ce que nous ajouterons au contexte :
export interface IIdentityContext { identity?: { sub: string; username: string; };}Notez que nous définissons une propriété supplémentaire optionnelle au contexte. tRPC gère la garantie que cela est défini dans les procédures qui ont correctement configuré ce middleware.
Ensuite, nous implémenterons le middleware lui-même. Il a la structure suivante :
export const createIdentityPlugin = () => { const t = initTRPC.context<...>().create(); return t.procedure.use(async (opts) => { // Add logic here to run before the procedure
const response = await opts.next(...);
// Add logic here to run after the procedure
return response; });};Dans notre cas, nous voulons extraire des détails sur l’utilisateur Cognito appelant. Nous le ferons en extrayant l’ID de sujet de l’utilisateur (ou “sub”) de l’événement API Gateway, et en récupérant les détails de l’utilisateur depuis Cognito. L’implémentation varie selon que l’événement a été fourni à notre fonction par une API REST ou une API HTTP :
import { CognitoIdentityProvider } from '@aws-sdk/client-cognito-identity-provider';import { initTRPC, TRPCError } from '@trpc/server';import type { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda';import type { 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: `Unable to determine calling user`, }); }
const { Users } = await cognito.listUsers({ // Assumes user pool id is configured in lambda environment UserPoolId: process.env.USER_POOL_ID!, Limit: 1, Filter: `sub="${sub}"`, });
if (!Users || Users.length !== 1) { throw new TRPCError({ code: 'FORBIDDEN', message: `No user found with subjectId ${sub}`, }); }
// Provide the identity to other procedures in the context 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 type { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda';import type { 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: `Unable to determine calling user`, }); }
const { Users } = await cognito.listUsers({ // Assumes user pool id is configured in lambda environment UserPoolId: process.env.USER_POOL_ID!, Limit: 1, Filter: `sub="${sub}"`, });
if (!Users || Users.length !== 1) { throw new TRPCError({ code: 'FORBIDDEN', message: `No user found with subjectId ${sub}`, }); }
// Provide the identity to other procedures in the context return await opts.next({ ctx: { ...opts.ctx, identity: { sub, username: Users[0].Username!, }, }, }); });};Lorsque vous déployez avec auth: 'cognito', l’autorisateur Cognito d’API Gateway vérifie le JWT que l’appelant fournit dans l’en-tête Authorization et place les revendications vérifiées sur l’événement Lambda. Notre middleware lit simplement ces revendications — pas d’appels SDK AWS supplémentaires, pas de vérification JWT manuelle.
Tout d’abord, nous définissons ce que nous ajouterons au contexte :
export interface IIdentityContext { identity?: { sub: string; username: string; };}Notez que nous définissons une propriété supplémentaire optionnelle sur le contexte. tRPC gère la garantie que cela est défini dans les procédures qui ont correctement configuré ce middleware.
Ensuite, le middleware lui-même. Le type d’événement et l’emplacement des revendications diffèrent entre une API REST et une API HTTP, donc l’implémentation dépend de votre infra sélectionné :
L’autorisateur Cognito User Pools d’une API REST place les revendications à event.requestContext.authorizer.claims :
import { initTRPC, TRPCError } from '@trpc/server';import type { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda';import type { APIGatewayProxyEvent } from 'aws-lambda';
export interface IIdentityContext { identity?: { sub: string; username: string; };}
export const createIdentityPlugin = () => { const t = initTRPC .context< IIdentityContext & CreateAWSLambdaContextOptions<APIGatewayProxyEvent> >() .create();
return t.procedure.use(async (opts) => { const claims = opts.ctx.event.requestContext?.authorizer?.claims as | Record<string, string> | undefined;
const sub = claims?.sub; const username = claims?.username ?? claims?.['cognito:username'];
if (!sub || !username) { throw new TRPCError({ code: 'FORBIDDEN', message: 'Unable to determine calling user', }); }
return await opts.next({ ctx: { ...opts.ctx, identity: { sub, username, }, }, }); });};L’autorisateur JWT d’une API HTTP fournit un événement payload-v2 dont les revendications se trouvent un niveau plus profond, à event.requestContext.authorizer.jwt.claims. Le contexte doit être typé sur APIGatewayProxyEventV2WithJWTAuthorizer pour correspondre à celui que le publicProcedure généré utilise — sinon .concat() échoue avec l’erreur Context mismatch de tRPC :
import { initTRPC, TRPCError } from '@trpc/server';import type { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda';import type { APIGatewayProxyEventV2WithJWTAuthorizer } from 'aws-lambda';
export interface IIdentityContext { identity?: { sub: string; username: string; };}
export const createIdentityPlugin = () => { const t = initTRPC .context< IIdentityContext & CreateAWSLambdaContextOptions<APIGatewayProxyEventV2WithJWTAuthorizer> >() .create();
return t.procedure.use(async (opts) => { const claims = opts.ctx.event.requestContext?.authorizer?.jwt?.claims as | Record<string, string> | undefined;
const sub = claims?.sub; const username = claims?.username ?? claims?.['cognito:username'];
if (!sub || !username) { throw new TRPCError({ code: 'FORBIDDEN', message: 'Unable to determine calling user', }); }
return await opts.next({ ctx: { ...opts.ctx, identity: { sub, username, }, }, }); });};Vous pouvez ensuite mélanger le plugin dans n’importe quelle procédure qui a besoin de l’identité de l’appelant :
import { publicProcedure } from '../init.js';import { createIdentityPlugin } from '../middleware/identity.js';import { z } from 'zod';
export const me = publicProcedure .concat(createIdentityPlugin()) .output(z.object({ sub: z.string(), username: z.string() })) .query(({ ctx }) => ({ sub: ctx.identity!.sub, username: ctx.identity!.username, }));Déployer votre API tRPC
Section intitulée « Déployer votre API tRPC »Le générateur d’API tRPC crée une infrastructure en tant que code CDK ou Terraform en fonction de votre iac sélectionné. Vous pouvez l’utiliser pour déployer votre API tRPC.
Le construct CDK pour déployer votre API se trouve dans le dossier common/constructs. Vous pouvez le consommer dans une application CDK, par exemple :
import { MyApi } from '@my-scope/common-constructs';
export class ExampleStack extends Stack { constructor(scope: Construct, id: string) { // Add the api to your stack const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), }); }}import { MyApi, UserIdentity } from '@my-scope/common-constructs';
export class ExampleStack extends Stack { constructor(scope: Construct, id: string) { // Add the api to your stack const identity = new UserIdentity(this, 'Identity');
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), identity, }); }}Le construct UserIdentity peut être généré en utilisant le générateur ts#website#auth.
Cela configure votre infrastructure d’API, incluant une API AWS API Gateway REST ou HTTP, des fonctions AWS Lambda pour la logique métier, et l’authentification basée sur votre méthode auth choisie.
Les modules Terraform pour déployer votre API sont dans le dossier common/terraform. Vous pouvez l’utiliser dans une configuration Terraform.
Le module API stocke son zip de déploiement Lambda dans un bucket S3 d’actifs partagé — voir le guide d’infrastructure Terraform pour plus de détails. Instanciez le module core/asset-bucket une fois par déploiement et passez sa sortie bucket_name dans chaque module API / Lambda via l’entrée asset_bucket_name :
module "asset_bucket" { source = "../../common/terraform/src/core/asset-bucket"}
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# Environment variables for the Lambda function env = { ENVIRONMENT = var.environment LOG_LEVEL = "INFO" }
# Additional IAM policies if needed additional_iam_policy_statements = [ # Add any additional permissions your API needs ]
tags = local.common_tags}module "asset_bucket" { source = "../../common/terraform/src/core/asset-bucket"}
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
user_pool_id = local.user_pool_id user_pool_client_ids = [local.client_id]
# Environment variables for the Lambda function env = { ENVIRONMENT = var.environment LOG_LEVEL = "INFO" }
# Additional IAM policies if needed additional_iam_policy_statements = [ # Add any additional permissions your API needs ]
tags = local.common_tags}Vous pouvez configurer le Cognito User Pool et le Client en utilisant les ressources ou modules Terraform appropriés.
Cela configure :
- Une fonction AWS Lambda qui sert toutes les procédures tRPC
- API Gateway HTTP/REST API comme déclencheur de fonction
- Rôles et permissions IAM
- Groupe de journaux CloudWatch
- Configuration du traçage X-Ray
- Configuration CORS
Le module Terraform fournit plusieurs sorties que vous pouvez utiliser :
# Access the API endpointoutput "api_url" { value = module.my_api.stage_invoke_url}
# Access Lambda function detailsoutput "lambda_function_name" { value = module.my_api.lambda_function_name}
# Access IAM role for granting additional permissionsoutput "lambda_execution_role_arn" { value = module.my_api.lambda_execution_role_arn}Vous pouvez personnaliser les paramètres CORS en passant des variables au module :
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# Custom CORS configuration cors_allow_origins = ["https://myapp.com", "https://staging.myapp.com"] cors_allow_methods = ["GET", "POST", "PUT", "DELETE"] cors_allow_headers = [ "authorization", "content-type", "x-custom-header" ]
tags = local.common_tags}Pour les API REST, le construct généré associe par défaut une Web ACL AWS WAFv2 à l’étape API Gateway. La Web ACL utilise l’ensemble de règles par défaut géré par AWS (AWSManagedRulesCommonRuleSet et AWSManagedRulesKnownBadInputsRuleSet), offrant une protection contre les exploits web courants, y compris le Top 10 OWASP. Les journaux de requêtes WAF sont écrits dans un groupe CloudWatch Logs.
Vous pouvez modifier le construct rest-api généré pour ajouter, supprimer ou ajuster des règles (par exemple, pour ajouter des règles basées sur le taux ou des groupes de règles gérées supplémentaires).
Pour désactiver cette fonctionnalité (par exemple, pour attacher votre propre Web ACL), définissez enableWaf sur false :
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), enableWaf: false,});Pour désactiver cette fonctionnalité (par exemple, pour attacher votre propre Web ACL), définissez enable_waf sur false :
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name enable_waf = false}Journalisation des accès
Section intitulée « Journalisation des accès »Pour les API REST, l’infrastructure générée active la journalisation des accès par défaut, écrivant une ligne JSON structurée par requête dans un groupe CloudWatch Logs dédié. Le groupe de logs est chiffré avec une clé KMS gérée par le client et conservé pendant un an.
API Gateway écrit les logs d’accès en utilisant un rôle CloudWatch Logs au niveau du compte. Ce rôle est configuré sur le paramètre AWS::ApiGateway::Account, qui est un singleton par région par compte — il n’y a qu’un seul rôle pour chaque API REST dans la région. Pour gérer cela en toute sécurité à travers plusieurs stacks déployées indépendamment, l’infrastructure générée :
- Crée un rôle CloudWatch Logs partagé et le configure sur le compte uniquement lorsqu’aucun rôle fonctionnel n’est déjà défini, de sorte que les déploiements n’écrasent jamais un rôle appartenant à une autre stack.
- Laisse le paramètre du compte intact lors du démontage, de sorte que la destruction d’une stack ne désactive jamais la journalisation pour d’autres API REST dans la région.
Le rôle du compte est géré par le construct ApiGatewayAccount, un singleton à portée de stack résolu via ApiGatewayAccount.ensure(scope). Le stage de chaque API REST en dépend, et le rôle est configuré par une ressource personnalisée basée sur Lambda.
Le format du log d’accès est défini par le construct RestApi que votre API étend. Pour le personnaliser, passez deployOptions à super dans le fichier généré packages/common/constructs/src/app/apis/my-api.ts, en conservant le tracingEnabled que le construct définit déjà :
super(scope, id, { apiName: 'MyApi', // ... deployOptions: { tracingEnabled: true, accessLogFormat: AccessLogFormat.clf(), }, ...props,});AccessLogFormat est importé depuis aws-cdk-lib/aws-apigateway. Tout ce que vous ne définissez pas conserve la valeur par défaut du construct — un format JSON avec les champs standard.
Le rôle du compte est géré par le module core/api/api-gateway-account, qui est instancié par le module API généré. Il configure le compte de manière idempotente et n’est jamais réinitialisé lors d’un terraform destroy.
Vous pouvez personnaliser le format du log d’accès en modifiant le bloc access_log_settings sur la ressource aws_api_gateway_stage dans le module API généré.
Intégrations
Section intitulée « Intégrations »Les constructs CDK d’API REST/HTTP sont configurés pour fournir une interface type-safe pour définir des intégrations pour chacune de vos opérations.
Intégrations par défaut
Section intitulée « Intégrations par défaut »Vous pouvez utiliser la méthode statique defaultIntegrations pour utiliser le modèle par défaut, qui définit une fonction AWS Lambda individuelle pour chaque opération :
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});Le module généré définit déjà les intégrations par défaut pour le modèle avec lequel l’API a été générée, donc aucune configuration supplémentaire n’est nécessaire :
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
tags = local.common_tags}Avec le modèle isolated par défaut, cela crée une fonction Lambda par opération.
Accès aux intégrations
Section intitulée « Accès aux intégrations »Vous pouvez accéder aux fonctions AWS Lambda sous-jacentes via la propriété integrations du construct API, de manière type-safe. Par exemple, si votre API définit une opération nommée sayHello et que vous devez ajouter des permissions à cette fonction, vous pouvez le faire comme suit :
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
// sayHello is typed to the operations defined in your APIapi.integrations.sayHello.handler.addToRolePolicy(new PolicyStatement({ effect: Effect.ALLOW, actions: [...], resources: [...],}));Si votre API utilise le modèle shared, le Lambda de routeur partagé est exposé comme api.integrations.$router :
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
api.integrations.$router.handler.addEnvironment('LOG_LEVEL', 'DEBUG');Notez que $router n’est plus disponible si vous remplacez chaque opération via withOverrides, puisqu’aucune opération n’utilise plus l’intégration de routeur par défaut.
Avec le modèle isolated, les sorties du module sont des maps indexées par nom d’opération, vous pouvez donc accéder aux ressources d’une seule opération. Par exemple, pour accorder des permissions supplémentaires à la fonction Lambda d’une opération :
# Grant additional permissions to just the sayHello operation's functionresource "aws_iam_role_policy" "say_hello_permissions" { name = "say-hello-additional-permissions" role = module.my_api.lambda_execution_role_names["sayHello"]
policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "s3:GetObject", "s3:PutObject" ] Resource = "arn:aws:s3:::my-bucket/*" } ] })}Pour accorder les mêmes permissions à toutes les opérations, itérez sur la sortie operations :
resource "aws_iam_role_policy" "additional_permissions" { for_each = toset(module.my_api.operations)
name = "additional-api-permissions" role = module.my_api.lambda_execution_role_names[each.key]
policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = ["s3:GetObject"] Resource = "arn:aws:s3:::my-bucket/*" } ] })}Le module expose également lambda_function_names, lambda_function_arns, lambda_invoke_arns, integration_ids et lambda_log_group_names comme des maps indexées par nom d’opération. Avec le modèle shared, les sorties singulières équivalentes (lambda_execution_role_name, lambda_function_name, …) sont exposées à la place, puisqu’il n’y a qu’une seule fonction.
Les permissions dont chaque opération a besoin sont mieux passées au module, qui les applique au rôle de chaque fonction :
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
additional_iam_policy_statements = [ { Effect = "Allow" Action = ["s3:GetObject"] Resource = ["arn:aws:s3:::my-bucket/*"] } ]}Personnalisation des options par défaut
Section intitulée « Personnalisation des options par défaut »Si vous souhaitez personnaliser les options utilisées lors de la création de la fonction Lambda pour chaque intégration par défaut, vous pouvez utiliser la méthode withDefaultOptions. Par exemple, si vous souhaitez que toutes vos fonctions Lambda résident dans un Vpc :
const vpc = new Vpc(this, 'Vpc', ...);
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withDefaultOptions({ vpc, }) .build(),});La configuration VPC est déjà prise en charge par le module généré — définissez enable_vpc avec vpc_id et subnet_ids, et le module déploie chaque fonction Lambda dans votre VPC derrière un groupe de sécurité partagé qu’il crée pour vous :
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# VPC configuration enable_vpc = true vpc_id = aws_vpc.main.id subnet_ids = [aws_subnet.private_a.id, aws_subnet.private_b.id]
tags = local.common_tags}Pour les options que le module n’expose pas, modifiez directement la ressource aws_lambda_function dans le module Terraform généré. Avec le modèle isolated, cette ressource unique est déclarée for_each = local.operations, donc une modification s’applique à toutes les opérations.
Personnalisation des options par opération
Section intitulée « Personnalisation des options par opération »Pour personnaliser les options utilisées pour créer l’intégration par défaut pour des opérations spécifiques (sans affecter les autres), vous pouvez utiliser la méthode withOperationOptions. Par exemple, si vous souhaitez augmenter le timeout de la fonction Lambda pour une seule opération :
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOperationOptions({ sayHello: { timeout: Duration.seconds(60), }, }) .build(),});
// The selected operations remain default integrations, so they're still typed accordingly:api.integrations.sayHello.handler.addToRolePolicy(new PolicyStatement({ ... }));Les options que vous spécifiez sont fusionnées avec les options d’intégration par défaut (et toutes les options définies via withDefaultOptions). Notez que vous ne pouvez pas spécifier d’options pour les opérations que vous avez remplacées via withOverrides, car celles-ci n’utilisent plus l’intégration par défaut.
Vous rencontrerez une erreur de type si la même opération est ciblée à la fois par withOperationOptions et withOverrides, quel que soit l’ordre dans lequel vous les appelez.
Avec le modèle isolated, la ressource de fonction Lambda est déjà par opération, donc les options peuvent varier selon le nom de l’opération. Par exemple, pour donner à une opération un timeout plus long, modifiez la ressource aws_lambda_function dans le module généré :
resource "aws_lambda_function" "api_lambda" { for_each = local.operations
# Default to 30 seconds, but allow longer for specific operations timeout = lookup({ sayHello = 60 }, each.key, 30)
# ... rest of configuration}Remplacement des intégrations
Section intitulée « Remplacement des intégrations »Vous pouvez également remplacer les intégrations pour des opérations spécifiques en utilisant la méthode withOverrides. Chaque remplacement doit spécifier une propriété integration qui est typée selon le construct d’intégration CDK approprié pour l’API HTTP ou REST. La méthode withOverrides est également type-safe. Par exemple, si vous souhaitez remplacer une API getDocumentation pour pointer vers de la documentation hébergée par un site web externe, vous pourriez le faire comme suit :
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), }, }) .build(),});Vous remarquerez également que l’intégration remplacée n’a plus de propriété handler lors de l’accès via api.integrations.getDocumentation.
Vous pouvez ajouter des propriétés supplémentaires à une intégration qui seront également typées en conséquence, permettant à d’autres types d’intégration d’être abstraits tout en restant type-safe, par exemple si vous avez créé une intégration S3 pour une API REST et souhaitez plus tard référencer le bucket pour une opération particulière, vous pouvez le faire comme suit :
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(),});
// Later, perhaps in another file, you can access the bucket property we defined// in a type-safe mannerapi.integrations.getFile.bucket.grantRead(...);Pour pointer une opération spécifique vers un type d’intégration différent, excluez-la du for_each par défaut et déclarez son intégration séparément. Par exemple, pour servir getDocumentation depuis un site web externe :
# Exclude the overridden operation from the default per-operation resourceslocals { overridden_operations = ["getDocumentation"] default_operations = { for op, details in local.operations : op => details if !contains(local.overridden_operations, op) }}
# Then use local.default_operations in place of local.operations for the# aws_lambda_function, aws_iam_role, aws_apigatewayv2_integration and# aws_lambda_permission resources, and add the override:resource "aws_apigatewayv2_integration" "get_documentation" { api_id = module.http_api.api_id integration_type = "HTTP_PROXY" integration_uri = "https://example.com/documentation" integration_method = "GET"}
resource "aws_apigatewayv2_route" "get_documentation" { api_id = module.http_api.api_id route_key = local.route_key["getDocumentation"] target = "integrations/${aws_apigatewayv2_integration.get_documentation.id}"}Remplacement des autorisateurs
Section intitulée « Remplacement des autorisateurs »Vous pouvez également fournir des options dans votre intégration pour remplacer des options de méthode particulières telles que les autorisateurs, par exemple si vous souhaitez utiliser l’authentification Cognito pour votre opération getDocumentation :
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), options: { authorizer: new CognitoUserPoolsAuthorizer(...) // for REST, or HttpUserPoolAuthorizer for an HTTP API } }, }) .build(),});L’autorisation est définie sur la route (API HTTP) ou la méthode (API REST) pour chaque opération, elle peut donc varier selon le nom de l’opération. Par exemple, pour laisser une opération non authentifiée sur une API HTTP :
resource "aws_apigatewayv2_route" "operation_routes" { for_each = local.operations
# ... rest of configuration
authorization_type = each.key == "getDocumentation" ? "NONE" : "AWS_IAM"}Pour une API REST authentifiée par IAM, ajoutez également une déclaration de politique de ressource autorisant l’accès non authentifié au chemin de cette opération.
Intégrations explicites
Section intitulée « Intégrations explicites »Si vous préférez, vous pouvez choisir de ne pas utiliser les intégrations par défaut et d’en fournir directement une pour chaque opération. Ceci est utile si, par exemple, chaque opération doit utiliser un type d’intégration différent ou si vous souhaitez recevoir une erreur de type lors de l’ajout de nouvelles opérations :
new MyApi(this, 'MyApi', { integrations: { sayHello: { integration: new LambdaIntegration(...), }, getDocumentation: { integration: new HttpIntegration(...), }, },});Remplacez le for_each utilisé par le modèle isolated par des instanciations explicites des fonctions Lambda, intégrations et permissions pour chaque opération.
Modèle d’intégration
Section intitulée « Modèle d’intégration »Les API générées prennent en charge deux modèles d’intégration :
isolatedcrée une fonction Lambda par opération. C’est l’option par défaut et recommandée pour les API.sharedcrée un seul Lambda de routeur par défaut et le réutilise pour chaque opération sauf si vous remplacez des intégrations spécifiques.
isolated vous donne des permissions et une configuration plus granulaires par opération, ainsi qu’une meilleure séparation pour les logs et les traces. shared réduit la probabilité de rencontrer des démarrages à froid pour les API à faible utilisation.
Le modèle d’intégration peut être changé à tout moment dans CDK en mettant à jour votre construct API. Par exemple, définir pattern à 'shared' crée une seule fonction au lieu d’une par intégration :
export class MyApi<...> extends ... {
public static defaultIntegrations = (scope: Construct) => { ... return IntegrationBuilder.rest({ pattern: 'shared', ... }); };}Contrairement à CDK, le modèle d’intégration est intégré dans le module généré. Pour changer le modèle d’intégration :
- Supprimez le module API précédemment généré dans
packages/common/terraform/src/app/apis - Relancez le générateur qui a créé votre API avec l’autre modèle d’intégration (par exemple
--integrationPattern=shared)
Avec le modèle isolated, le module lit les opérations depuis un fichier généré :
locals { operations_file = "${path.module}/../../../generated/my-api/operations.json" operations = fileexists(local.operations_file) ? jsondecode(file(local.operations_file)) : {}}Ce fichier est généré à partir de votre API, vous n’avez donc pas besoin de le modifier manuellement. L’ajout d’une opération à votre code d’application API ajoute la route et la fonction lambda lors du prochain déploiement. Il est .gitignored par défaut ; supprimez l’entrée si vous préférez le versionner.
Limite de profondeur de chemin de l’API REST Terraform
Section intitulée « Limite de profondeur de chemin de l’API REST Terraform »Accorder l’accès (IAM uniquement)
Section intitulée « Accorder l’accès (IAM uniquement) »Vous pouvez accorder l’accès à votre API comme suit :
api.grantInvokeAccess(myIdentityPool.authenticatedRole);# Create an IAM policy to allow invoking the APIresource "aws_iam_policy" "api_invoke_policy" { name = "MyApiInvokePolicy" description = "Policy to allow invoking the tRPC API"
policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = "execute-api:Invoke" Resource = "${module.my_api.api_execution_arn}/*/*" } ] })}
# Attach the policy to an IAM role (e.g., for authenticated users)resource "aws_iam_role_policy_attachment" "api_invoke_access" { role = aws_iam_role.authenticated_user_role.name policy_arn = aws_iam_policy.api_invoke_policy.arn}
# Or attach to an existing role by nameresource "aws_iam_role_policy_attachment" "api_invoke_access_existing" { role = "MyExistingRole" policy_arn = aws_iam_policy.api_invoke_policy.arn}Les sorties clés du module API que vous pouvez utiliser pour les politiques IAM sont :
module.my_api.api_execution_arn- Pour accorder les permissions execute-api:Invokemodule.my_api.api_arn- L’ARN de l’API Gatewaymodule.my_api.lambda_function_arn- L’ARN de la fonction Lambda
Cible de bundle
Section intitulée « Cible de bundle »Le générateur configure automatiquement une cible bundle qui utilise Rolldown pour créer un package de déploiement :
pnpm nx bundle <project-name>yarn nx bundle <project-name>npx nx bundle <project-name>bunx nx bundle <project-name>La configuration de Rolldown se trouve dans rolldown.config.ts, avec une entrée par bundle à générer. Rolldown gère la création de plusieurs bundles en parallèle s’ils sont définis.
Serveur tRPC local
Section intitulée « Serveur tRPC local »Vous pouvez utiliser la cible serve pour exécuter un serveur local pour votre API, par exemple :
pnpm nx serve my-apiyarn nx serve my-apinpx nx serve my-apibunx nx serve my-apiLe point d’entrée pour le serveur local est src/local-server.ts.
Cela se rechargera automatiquement lorsque vous apporterez des modifications à votre API.
Invoquer votre API tRPC
Section intitulée « Invoquer votre API tRPC »Vous pouvez créer un client tRPC pour invoquer votre API de manière type-safe. Si vous appelez votre API tRPC depuis un autre backend, vous pouvez utiliser le client dans src/client/index.ts, par exemple :
import { createMyApiClient } from '@my-scope/my-api';
const client = createMyApiClient({ url: 'https://my-api-url.example.com/' });
await client.echo.query({ message: 'Hello world!' });Si vous appelez votre API depuis un site web React, envisagez d’utiliser le générateur Connection pour configurer le client.
Plus d’informations
Section intitulée « Plus d’informations »Pour plus d’informations sur tRPC, veuillez vous référer à la documentation tRPC.
Connexions
Section intitulée « Connexions »Utilisez le générateur connection pour intégrer ce projet avec d’autres dans votre espace de travail. Les connexions suivantes impliquent ce projet :