tRPC
tRPC est un framework pour construire des APIs en TypeScript avec une sécurité de type de bout en bout. En utilisant tRPC, les mises à jour des entrées et sorties des opérations de l’API sont immédiatement reflétées dans le code client et visibles dans votre IDE sans avoir à reconstruire votre projet.
Le générateur d’API tRPC crée une nouvelle API tRPC avec une infrastructure configurée via 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 une validation de schéma avec Zod. Il configure AWS Lambda Powertools pour l’observabilité, incluant le logging, le tracing 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 :
- 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#trpc-api
- Remplissez les paramètres requis
- Cliquez sur
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
Vous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
pnpm nx g @aws/nx-plugin:ts#trpc-api --dry-run
yarn nx g @aws/nx-plugin:ts#trpc-api --dry-run
npx nx g @aws/nx-plugin:ts#trpc-api --dry-run
bunx nx g @aws/nx-plugin:ts#trpc-api --dry-run
Paramètre | Type | Par défaut | Description |
---|---|---|---|
name Requis | 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. |
iacProvider | string | CDK | The preferred IaC provider |
Résultat du générateur
Section intitulée « Résultat du générateur »Le générateur créera la structure de projet suivante dans le répertoire <directory>/<api-name>
:
Répertoiresrc
- init.ts Initialisation du backend tRPC
- router.ts Définition du routeur tRPC (point d’entrée de l’API du gestionnaire Lambda)
Répertoireschema Définitions de schémas avec Zod
- echo.ts Exemple de définitions pour l’entrée et la sortie de la procédure “echo”
Répertoireprocedures Procédures (ou opérations) exposées par votre API
- echo.ts Exemple de procédure
Répertoiremiddleware
- error.ts Middleware pour la gestion des erreurs
- logger.ts middleware pour configurer AWS Powertools pour le logging Lambda
- tracer.ts middleware pour configurer AWS Powertools pour le tracing Lambda
- metrics.ts middleware pour configurer AWS Powertools pour les métriques Lambda
- local-server.ts Point d’entrée de l’adaptateur autonome tRPC pour le serveur de développement local
Répertoireclient
- index.ts Client typé pour les appels API entre machines
- tsconfig.json Configuration TypeScript
- project.json Configuration du projet et cibles de build
Infrastructure
Section intitulée « Infrastructure »Ce générateur fournit de l’infrastructure as code basée sur votre iacProvider
choisi. Il créera un projet dans packages/common
qui inclut les constructions CDK ou modules Terraform pertinents.
Le projet commun d’infrastructure as code est structuré comme suit :
Répertoirepackages/common/constructs
Répertoiresrc
Répertoireapp/ Constructions pour l’infrastructure spécifique à un projet/générateur
- …
Répertoirecore/ Constructions génériques réutilisées par celles dans
app
- …
- index.ts Point d’entrée exportant les constructions depuis
app
- project.json Cibles de build et configuration du projet
Répertoirepackages/common/terraform
Répertoiresrc
Répertoireapp/ Modules Terraform pour l’infrastructure spécifique à un projet/générateur
- …
Répertoirecore/ Modules génériques réutilisés par ceux dans
app
- …
- project.json Cibles de build et configuration du projet
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 Construct CDK pour déployer votre API
Répertoirecore
Répertoireapi
- http-api.ts Construct CDK pour déployer une API HTTP (si vous avez choisi de déployer une API HTTP)
- rest-api.ts Construct CDK pour déployer une API REST (si vous avez choisi de déployer une API REST)
- utils.ts Utilitaires pour les constructs d’API
Répertoirepackages/common/terraform/src
Répertoireapp
Répertoireapis
Répertoire<project-name>
- <project-name>.tf Module pour déployer votre API
Répertoirecore
Répertoireapi
Répertoirehttp-api
- http-api.tf Module pour déployer une API HTTP (si vous avez choisi de déployer une API HTTP)
Répertoirerest-api
- rest-api.tf Module pour déployer une API REST (si vous avez choisi de déployer une API REST)
Implémentation de votre API tRPC
Section intitulée « Implémentation de votre API tRPC »À haut niveau, les APIs 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 partagés entre votre code client et serveur. Dans ce package, ces types sont définis avec Zod, une bibliothèque de déclaration et validation de schémas orientée TypeScript.
Un exemple de schéma pourrait ressembler à ceci :
import { z } from 'zod';
// Définition du schémaexport const UserSchema = z.object({ name: z.string(), height: z.number(), dateOfBirth: z.string().datetime(),});
// Type TypeScript correspondantexport type User = z.TypeOf<typeof UserSchema>;
Avec 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 entre le code serveur et client, fournissant un seul endroit à modifier lors de changements des structures utilisées dans votre API.
Les schémas sont automatiquement validés par votre API tRPC à l’exécution, évitant d’avoir à écrire manuellement une logique de validation personnalisée dans votre backend.
Zod fournit des utilitaires puissants pour combiner ou dériver des schémas comme .merge
, .pick
, .omit
et plus encore. Vous trouverez plus d’informations sur le site de documentation de Zod.
Routeur et procédures
Section intitulée « Routeur et procédures »Vous trouverez le point d’entrée de votre API dans src/router.ts
. Ce fichier contient le gestionnaire Lambda qui achemine les requêtes vers des “procédures” basées sur l’opération invoquée. Chaque procédure définit l’entrée attendue, la sortie et l’implémentation.
Le routeur 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) => ({ result: opts.input.message }));
Décomposons ce code :
publicProcedure
définit une méthode publique sur l’API, incluant le middleware configuré danssrc/middleware
. Ce middleware inclut l’intégration AWS Lambda Powertools pour le logging, tracing et les métriques.input
accepte un schéma Zod définissant l’entrée attendue pour l’opération. Les requêtes envoyées pour cette opération sont automatiquement validées contre ce schéma.output
accepte un schéma Zod définissant 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.query
accepte une fonction définissant l’implémentation de votre API. Cette implémentation reçoitopts
, qui contient l’input
passé à votre opération, ainsi que d’autres contextes configurés par le middleware, disponibles dansopts.ctx
. La fonction passée àquery
doit 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 cela 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
.
Personnalisation de votre API tRPC
Section intitulée « Personnalisation de votre API tRPC »Dans votre implémentation, vous pouvez retourner des réponses d’erreur aux clients en lançant un TRPCError
. Ceux-ci acceptent un code
indiquant le type d’erreur, par exemple :
throw new TRPCError({ code: 'NOT_FOUND', message: 'La ressource demandée n\'a pas pu être trouvée',});
Organisation des opérations
Section intitulée « Organisation des opérations »Au fur et à mesure que votre API grandit, vous pouvez souhaiter regrouper des opérations liées.
Vous pouvez regrouper des opérations avec 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 invoquer l’opération listUsers
ressemblerait à ceci :
client.users.list.query();
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 logger dans CloudWatch Logs, et/ou contrôler les valeurs supplémentaires à inclure dans chaque message de log structuré. Par exemple :
export const echo = publicProcedure .input(...) .output(...) .query(async (opts) => { opts.ctx.logger.info('Opération appelée avec l\'entrée', opts.input);
return ...; });
Pour plus d’informations sur le logger, référez-vous à la documentation AWS Lambda Powertools Logger.
Enregistrement de métriques
Section intitulée « Enregistrement de 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 les utiliser pour enregistrer des métriques dans CloudWatch sans avoir à importer et utiliser l’AWS SDK, par exemple :
export const echo = publicProcedure .input(...) .output(...) .query(async (opts) => { opts.ctx.metrics.addMetric('Invocations', 'Count', 1);
return ...; });
Pour plus d’informations, référez-vous à la documentation AWS Lambda Powertools Metrics.
Ajustement fin du tracing X-Ray
Section intitulée « Ajustement fin du tracing 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 insights détaillés 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'); // ... logique de mon algorithme à capturer subSegment.close();
return ...; });
Pour plus d’informations, référez-vous à la documentation AWS Lambda Powertools Tracer.
Implémentation de middleware personnalisé
Section intitulée « Implémentation de middleware personnalisé »Vous pouvez ajouter des valeurs supplémentaires au contexte fourni aux procédures en implémentant un middleware.
Par exemple, implémentons un middleware pour extraire des détails sur l’utilisateur appelant notre API dans src/middleware/identity.ts
.
Cet exemple suppose que auth
était configuré à IAM
. Pour l’authentification Cognito, le middleware d’identité est plus simple, extrayant les claims pertinents depuis l’event
.
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é optionnelle supplémentaire au contexte. tRPC s’assure que cette propriété est définie dans les procédures ayant correctement configuré ce middleware.
Ensuite, nous implémentons le middleware lui-même. Il a la structure suivante :
export const createIdentityPlugin = () => { const t = initTRPC.context<...>().create(); return t.procedure.use(async (opts) => { // Ajoutez ici la logique à exécuter avant la procédure
const response = await opts.next(...);
// Ajoutez ici la logique à exécuter après la procédure
return response; });};
Dans notre cas, nous voulons extraire les détails de l’utilisateur Cognito appelant. Nous le ferons en extrayant l’ID de sujet (ou “sub”) de l’utilisateur depuis l’événement API Gateway, et en récupérant les détails de l’utilisateur depuis Cognito. L’implémentation varie légèrement selon que l’événement est fourni par une API REST ou HTTP :
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: `Impossible de déterminer l'utilisateur appelant`, }); }
const { Users } = await cognito.listUsers({ // Suppose que l'ID du pool utilisateur est configuré dans l'environnement Lambda UserPoolId: process.env.USER_POOL_ID!, Limit: 1, Filter: `sub="${sub}"`, });
if (!Users || Users.length !== 1) { throw new TRPCError({ code: 'FORBIDDEN', message: `Aucun utilisateur trouvé avec l'ID de sujet ${sub}`, }); }
// Fournit l'identité aux autres procédures dans le contexte 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: `Impossible de déterminer l'utilisateur appelant`, }); }
const { Users } = await cognito.listUsers({ // Suppose que l'ID du pool utilisateur est configuré dans l'environnement Lambda UserPoolId: process.env.USER_POOL_ID!, Limit: 1, Filter: `sub="${sub}"`, });
if (!Users || Users.length !== 1) { throw new TRPCError({ code: 'FORBIDDEN', message: `Aucun utilisateur trouvé avec l'ID de sujet ${sub}`, }); }
// Fournit l'identité aux autres procédures dans le contexte return await opts.next({ ctx: { ...opts.ctx, identity: { sub, username: Users[0].Username!, }, }, }); });};
Déploiement de votre API tRPC
Section intitulée « Déploiement de votre API tRPC »Le générateur d’API tRPC crée une infrastructure CDK ou Terraform en fonction du iacProvider
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 l’utiliser dans une application CDK, par exemple :
import { MyApi } from ':my-scope/common-constructs`;
export class ExampleStack extends Stack { constructor(scope: Construct, id: string) { // Ajoutez l'API à votre stack const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), }); }}
Ceci configure l’infrastructure de votre API, incluant une API REST ou HTTP AWS API Gateway, des fonctions AWS Lambda pour la logique métier, et l’authentification selon la méthode auth
choisie.
Si vous avez choisi d’utiliser l’authentification Cognito
, vous devrez fournir la propriété identity
au construct API :
import { MyApi, UserIdentity } from ':my-scope/common-constructs';
export class ExampleStack extends Stack { constructor(scope: Construct, id: string) { 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#react-website-auth
Les modules Terraform pour déployer votre API se trouvent dans le dossier common/terraform
. Vous pouvez les utiliser dans une configuration Terraform :
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
# Variables d'environnement pour la fonction Lambda env = { ENVIRONMENT = var.environment LOG_LEVEL = "INFO" }
# Politiques IAM supplémentaires si nécessaires additional_iam_policy_statements = [ # Ajoutez les permissions supplémentaires requises par votre API ]
tags = local.common_tags}
Ceci configure :
- Une fonction AWS Lambda qui sert toutes les procédures tRPC
- Une API Gateway HTTP/REST comme déclencheur de la fonction
- Les rôles et permissions IAM
- Un groupe de logs CloudWatch
- La configuration du tracing X-Ray
- La configuration CORS
Si vous avez choisi d’utiliser l’authentification Cognito
, vous devrez fournir la configuration Cognito :
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
user_pool_id = local.user_pool_id user_pool_client_ids = [local.client_id]
env = { ENVIRONMENT = var.environment LOG_LEVEL = "INFO" }
tags = local.common_tags}
Vous pouvez configurer le User Pool et Client Cognito en utilisant les ressources ou modules Terraform appropriés.
Le module Terraform fournit plusieurs sorties utilisables :
# Accéder à l'URL de l'APIoutput "api_url" { value = module.my_api.stage_invoke_url}
# Accéder aux détails de la fonction Lambdaoutput "lambda_function_name" { value = module.my_api.lambda_function_name}
# Accéder au rôle IAM pour accorder des permissions supplémentairesoutput "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"
# Configuration CORS personnalisée 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}
Si vous avez choisi None
pour auth
lors de l’exécution du générateur, vous pourriez voir des échecs de vérification Checkov comme :
Check: CKV_AWS_309: "Ensure API GatewayV2 routes specify an authorization type" FAILED for resource: aws_apigatewayv2_route.proxy_routes["PUT"]
Check: CKV_AWS_59: "Ensure there is no open access to back-end resources through API" FAILED for resource: aws_api_gateway_method.proxy_method
Vous pouvez ajouter un commentaire de suppression si vous êtes sûr de vouloir que votre API soit publique.
Intégrations
Section intitulée « Intégrations »Les constructs CDK pour l’API REST/HTTP sont configurés pour fournir une interface typée permettant de définir des intégrations pour chacune de vos opérations.
Les constructs CDK offrent un support complet d’intégration typée comme décrit ci-dessous.
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(),});
Les modules Terraform utilisent automatiquement le modèle router avec une seule fonction Lambda. Aucune configuration supplémentaire n’est nécessaire :
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
# Le module crée automatiquement une fonction Lambda unique # qui gère toutes les opérations de l'API tags = local.common_tags}
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 typée. 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 est typé selon les opérations définies dans votre APIapi.integrations.sayHello.handler.addToRolePolicy(new PolicyStatement({ effect: Effect.ALLOW, actions: [...], resources: [...],}));
Avec le modèle router de Terraform, il n’y a qu’une seule fonction Lambda. Vous pouvez y accéder via les sorties du module :
# Accorder des permissions supplémentaires à la fonction Lambda uniqueresource "aws_iam_role_policy" "additional_permissions" { name = "additional-api-permissions" role = module.my_api.lambda_execution_role_name
policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "s3:GetObject", "s3:PutObject" ] 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 des fonctions Lambda pour chaque intégration par défaut, vous pouvez utiliser la méthode withDefaultOptions
. Par exemple, si vous voulez 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(),});
Pour personnaliser des options comme la configuration VPC, vous devez modifier le module Terraform généré. Par exemple, pour ajouter le support VPC à toutes les fonctions Lambda :
# Ajouter des variables VPCvariable "vpc_subnet_ids" { description = "Liste des IDs de sous-réseaux VPC pour la fonction Lambda" type = list(string) default = []}
variable "vpc_security_group_ids" { description = "Liste des IDs de groupes de sécurité VPC pour la fonction Lambda" type = list(string) default = []}
# Mettre à jour la ressource Lambdaresource "aws_lambda_function" "api_lambda" { # ... configuration existante ...
# Ajouter la configuration VPC vpc_config { subnet_ids = var.vpc_subnet_ids security_group_ids = var.vpc_security_group_ids }}
Puis utiliser le module avec la configuration VPC :
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
# Configuration VPC vpc_subnet_ids = [aws_subnet.private_a.id, aws_subnet.private_b.id] vpc_security_group_ids = [aws_security_group.lambda_sg.id]
tags = local.common_tags}
Surcharge des intégrations
Section intitulée « Surcharge des intégrations »Vous pouvez aussi surcharger les intégrations pour des opérations spécifiques en utilisant la méthode withOverrides
. Chaque surcharge doit spécifier une propriété integration
typée selon le construct CDK d’intégration approprié pour l’API HTTP ou REST. La méthode withOverrides
est également typée. Par exemple, si vous voulez rediriger une API getDocumentation
vers une documentation hébergée sur un site externe :
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), }, }) .build(),});
Vous remarquerez que l’intégration surchargée n’a plus de propriété handler
lorsqu’on y accède via api.integrations.getDocumentation
.
Vous pouvez ajouter des propriétés supplémentaires à une intégration qui seront également typées, permettant d’abstraire d’autres types d’intégration tout en conservant le typage. Par exemple, si vous avez créé une intégration S3 pour une API REST et que vous souhaitez référencer le bucket pour une opération particulière :
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(),});
// Plus tard, peut-être dans un autre fichier, vous pouvez accéder à la propriété bucket// que nous avons définie de manière typéeapi.integrations.getFile.bucket.grantRead(...);
Surcharge des autorisations
Section intitulée « Surcharge des autorisations »Vous pouvez aussi fournir des options
dans votre intégration pour surcharger des options de méthode spécifiques comme les autorisations. 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(...) // pour REST, ou HttpUserPoolAuthorizer pour une API HTTP } }, }) .build(),});
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 fournir directement une intégration pour chaque opération. Ceci est utile si, par exemple, chaque opération nécessite un type d’intégration différent ou si vous voulez obtenir 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(...), }, },});
Pour des intégrations explicites par opération avec Terraform, vous devez modifier le module généré spécifique à l’application pour remplacer l’intégration proxy par défaut par des intégrations spécifiques à chaque opération.
Modifiez packages/common/terraform/src/app/apis/my-api/my-api.tf
:
- Supprimer les routes proxy par défaut (ex:
resource "aws_apigatewayv2_route" "proxy_routes"
) - Remplacer la fonction Lambda unique par des fonctions individuelles pour chaque opération
- Créer des intégrations et routes spécifiques pour chaque opération, en réutilisant le même bundle ZIP :
# Supprimer la fonction Lambda unique par défaut resource "aws_lambda_function" "api_lambda" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApiHandler" role = aws_iam_role.lambda_execution_role.arn handler = "index.handler" runtime = "nodejs22.x" timeout = 30 # ... reste de la configuration }
# Supprimer l'intégration proxy par défaut resource "aws_apigatewayv2_integration" "lambda_integration" { api_id = module.http_api.api_id integration_type = "AWS_PROXY" integration_uri = aws_lambda_function.api_lambda.invoke_arn # ... reste de la configuration }
# Supprimer les routes proxy par défaut resource "aws_apigatewayv2_route" "proxy_routes" { for_each = toset(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]) api_id = module.http_api.api_id route_key = "${each.key} /{proxy+}" target = "integrations/${aws_apigatewayv2_integration.lambda_integration.id}" # ... reste de la configuration }
# Ajouter des fonctions Lambda individuelles pour chaque opération en utilisant le même bundle resource "aws_lambda_function" "say_hello_handler" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApi-SayHello" role = aws_iam_role.lambda_execution_role.arn handler = "sayHello.handler" # Handler spécifique pour cette opération runtime = "nodejs22.x" timeout = 30 source_code_hash = data.archive_file.lambda_zip.output_base64sha256
tracing_config { mode = "Active" }
environment { variables = merge({ AWS_CONNECTION_REUSE_ENABLED = "1" }, var.env) }
tags = var.tags }
resource "aws_lambda_function" "get_documentation_handler" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApi-GetDocumentation" role = aws_iam_role.lambda_execution_role.arn handler = "getDocumentation.handler" # Handler spécifique pour cette opération runtime = "nodejs22.x" timeout = 30 source_code_hash = data.archive_file.lambda_zip.output_base64sha256
tracing_config { mode = "Active" }
environment { variables = merge({ AWS_CONNECTION_REUSE_ENABLED = "1" }, var.env) }
tags = var.tags }
# Ajouter des intégrations spécifiques pour chaque opération resource "aws_apigatewayv2_integration" "say_hello_integration" { api_id = module.http_api.api_id integration_type = "AWS_PROXY" integration_uri = aws_lambda_function.say_hello_handler.invoke_arn payload_format_version = "2.0" timeout_milliseconds = 30000 }
resource "aws_apigatewayv2_integration" "get_documentation_integration" { api_id = module.http_api.api_id integration_type = "HTTP_PROXY" integration_uri = "https://example.com/documentation" integration_method = "GET" }
# Ajouter des routes spécifiques pour chaque opération resource "aws_apigatewayv2_route" "say_hello_route" { api_id = module.http_api.api_id route_key = "POST /sayHello" target = "integrations/${aws_apigatewayv2_integration.say_hello_integration.id}" authorization_type = "AWS_IAM" }
resource "aws_apigatewayv2_route" "get_documentation_route" { api_id = module.http_api.api_id route_key = "GET /documentation" target = "integrations/${aws_apigatewayv2_integration.get_documentation_integration.id}" authorization_type = "NONE" }
# Ajouter des permissions Lambda pour chaque fonction resource "aws_lambda_permission" "say_hello_permission" { statement_id = "AllowExecutionFromAPIGateway-SayHello" action = "lambda:InvokeFunction" function_name = aws_lambda_function.say_hello_handler.function_name principal = "apigateway.amazonaws.com" source_arn = "${module.http_api.api_execution_arn}/*/*" }
resource "aws_lambda_permission" "get_documentation_permission" { statement_id = "AllowExecutionFromAPIGateway-GetDocumentation" action = "lambda:InvokeFunction" function_name = aws_lambda_function.get_documentation_handler.function_name principal = "apigateway.amazonaws.com" source_arn = "${module.http_api.api_execution_arn}/*/*" }
# Supprimer la fonction Lambda unique par défaut resource "aws_lambda_function" "api_lambda" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApiHandler" role = aws_iam_role.lambda_execution_role.arn handler = "index.handler" runtime = "nodejs22.x" timeout = 30 # ... reste de la configuration }
# Supprimer l'intégration proxy par défaut resource "aws_apigatewayv2_integration" "lambda_integration" { api_id = module.http_api.api_id integration_type = "AWS_PROXY" integration_uri = aws_lambda_function.api_lambda.invoke_arn # ... reste de la configuration }
# Supprimer les routes proxy par défaut resource "aws_apigatewayv2_route" "proxy_routes" { for_each = toset(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]) api_id = module.http_api.api_id route_key = "${each.key} /{proxy+}" target = "integrations/${aws_apigatewayv2_integration.lambda_integration.id}" # ... reste de la configuration }
# Ajouter des fonctions Lambda individuelles pour chaque opération en utilisant le même bundle resource "aws_lambda_function" "say_hello_handler" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApi-SayHello" role = aws_iam_role.lambda_execution_role.arn handler = "sayHello.handler" # Handler spécifique pour cette opération runtime = "nodejs22.x" timeout = 30 source_code_hash = data.archive_file.lambda_zip.output_base64sha256
tracing_config { mode = "Active" }
environment { variables = merge({ AWS_CONNECTION_REUSE_ENABLED = "1" }, var.env) }
tags = var.tags }
resource "aws_lambda_function" "get_documentation_handler" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApi-GetDocumentation" role = aws_iam_role.lambda_execution_role.arn handler = "getDocumentation.handler" # Handler spécifique pour cette opération runtime = "nodejs22.x" timeout = 30 source_code_hash = data.archive_file.lambda_zip.output_base64sha256
tracing_config { mode = "Active" }
environment { variables = merge({ AWS_CONNECTION_REUSE_ENABLED = "1" }, var.env) }
tags = var.tags }
# Ajouter des ressources et méthodes spécifiques pour chaque opération resource "aws_api_gateway_resource" "say_hello_resource" { rest_api_id = module.rest_api.api_id parent_id = module.rest_api.api_root_resource_id path_part = "sayHello" }
resource "aws_api_gateway_method" "say_hello_method" { rest_api_id = module.rest_api.api_id resource_id = aws_api_gateway_resource.say_hello_resource.id http_method = "POST" authorization = "AWS_IAM" }
resource "aws_api_gateway_integration" "say_hello_integration" { rest_api_id = module.rest_api.api_id resource_id = aws_api_gateway_resource.say_hello_resource.id http_method = aws_api_gateway_method.say_hello_method.http_method
integration_http_method = "POST" type = "AWS_PROXY" uri = aws_lambda_function.say_hello_handler.invoke_arn }
resource "aws_api_gateway_resource" "get_documentation_resource" { rest_api_id = module.rest_api.api_id parent_id = module.rest_api.api_root_resource_id path_part = "documentation" }
resource "aws_api_gateway_method" "get_documentation_method" { rest_api_id = module.rest_api.api_id resource_id = aws_api_gateway_resource.get_documentation_resource.id http_method = "GET" authorization = "NONE" }
resource "aws_api_gateway_integration" "get_documentation_integration" { rest_api_id = module.rest_api.api_id resource_id = aws_api_gateway_resource.get_documentation_resource.id http_method = aws_api_gateway_method.get_documentation_method.http_method
integration_http_method = "GET" type = "HTTP" uri = "https://example.com/documentation" }
# Mettre à jour le déploiement pour dépendre des nouvelles intégrations~ resource "aws_api_gateway_deployment" "api_deployment" { rest_api_id = module.rest_api.api_id
depends_on = [ aws_api_gateway_integration.lambda_integration, aws_api_gateway_integration.say_hello_integration, aws_api_gateway_integration.get_documentation_integration, ]
lifecycle { create_before_destroy = true }
triggers = { redeployment = sha1(jsonencode([ aws_api_gateway_integration.say_hello_integration, aws_api_gateway_integration.get_documentation_integration, ])) } }
# Ajouter des permissions Lambda pour chaque fonction resource "aws_lambda_permission" "say_hello_permission" { statement_id = "AllowExecutionFromAPIGateway-SayHello" action = "lambda:InvokeFunction" function_name = aws_lambda_function.say_hello_handler.function_name principal = "apigateway.amazonaws.com" source_arn = "${module.rest_api.api_execution_arn}/*/*" }
resource "aws_lambda_permission" "get_documentation_permission" { statement_id = "AllowExecutionFromAPIGateway-GetDocumentation" action = "lambda:InvokeFunction" function_name = aws_lambda_function.get_documentation_handler.function_name principal = "apigateway.amazonaws.com" source_arn = "${module.rest_api.api_execution_arn}/*/*" }
Modèle Router
Section intitulée « Modèle Router »Si vous préférez déployer une seule fonction Lambda pour gérer toutes les requêtes API, vous pouvez librement modifier la méthode defaultIntegrations
de votre API pour créer une seule fonction au lieu d’une par intégration :
export class MyApi<...> extends ... {
public static defaultIntegrations = (scope: Construct) => { const router = new Function(scope, 'RouterHandler', { ... }); return IntegrationBuilder.rest({ ... defaultIntegrationOptions: {}, buildDefaultIntegration: (op) => { return { # Référencer le même handler router dans chaque intégration integration: new LambdaIntegration(router), }; }, }); };}
Vous pouvez modifier le code selon vos préférences, par exemple définir la fonction router
comme paramètre de defaultIntegrations
au lieu de la construire dans la méthode.
Les modules Terraform utilisent automatiquement le modèle router - c’est l’approche par défaut et la seule supportée. Le module généré crée une fonction Lambda unique qui gère toutes les opérations API.
Vous pouvez simplement instancier le module par défaut pour obtenir le modèle router :
# Modèle router par défaut - fonction Lambda unique pour toutes les opérationsmodule "my_api" { source = "../../common/terraform/src/app/apis/my-api"
# La fonction Lambda unique gère automatiquement toutes les opérations tags = local.common_tags}
Si vous avez choisi CDK comme iacProvider
, lorsque vous ajoutez ou supprimez une procédure dans votre API tRPC, ces changements seront reflétés immédiatement dans le construct CDK sans avoir à reconstruire.
Octroi d’accès (IAM uniquement)
Section intitulée « Octroi d’accès (IAM uniquement) »Si vous avez choisi d’utiliser l’authentification IAM
, vous pouvez octroyer l’accès à votre API :
api.grantInvokeAccess(myIdentityPool.authenticatedRole);
# Créez une politique IAM pour autoriser l'appel de l'APIresource "aws_iam_policy" "api_invoke_policy" { name = "MyApiInvokePolicy" description = "Politique pour autoriser l'appel de l'API tRPC"
policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = "execute-api:Invoke" Resource = "${module.my_api.api_execution_arn}/*/*" } ] })}
# Attachez la politique à un rôle IAM (ex: pour utilisateurs authentifiés)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}
# Ou attachez à un rôle existant par nomresource "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 utilisables pour les politiques IAM sont :
module.my_api.api_execution_arn
- Pour octroyer 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
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 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
Le point d’entrée du serveur local est src/local-server.ts
.
Celui-ci se rechargera automatiquement lorsque vous modifierez votre API.
Appel de votre API tRPC
Section intitulée « Appel de votre API tRPC »Vous pouvez créer un client tRPC pour appeler votre API de manière typée. 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 React, envisagez d’utiliser le générateur Connexion API pour configurer le client.
Plus d’informations
Section intitulée « Plus d’informations »Pour plus d’informations sur tRPC, référez-vous à la documentation tRPC.