API TypeScript Smithy
Smithy est un langage de définition d’interface indépendant du protocole pour créer des API de manière pilotée par modèle.
Le générateur d’API TypeScript Smithy crée une nouvelle API utilisant Smithy pour la définition du service, et le Smithy TypeScript Server SDK pour l’implémentation. Le générateur fournit une infrastructure en tant que code CDK ou Terraform pour déployer votre service sur AWS Lambda, exposé via une API REST AWS API Gateway. Il offre un développement d’API type-safe avec génération automatique de code à partir des modèles Smithy. Le gestionnaire généré utilise AWS Lambda Powertools pour TypeScript 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 TypeScript Smithy
Section intitulée « Générer une API TypeScript Smithy »Vous pouvez générer une nouvelle API TypeScript Smithy de deux manières :
pnpm nx g @aws/nx-plugin:ts#api --framework=smithyyarn nx g @aws/nx-plugin:ts#api --framework=smithynpx nx g @aws/nx-plugin:ts#api --framework=smithybunx nx g @aws/nx-plugin:ts#api --framework=smithyVous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
pnpm nx g @aws/nx-plugin:ts#api --framework=smithy --dry-runyarn nx g @aws/nx-plugin:ts#api --framework=smithy --dry-runnpx nx g @aws/nx-plugin:ts#api --framework=smithy --dry-runbunx nx g @aws/nx-plugin:ts#api --framework=smithy --dry-run- 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
- framework: smithy
- Cliquez sur
Generate
| Paramètre | Type | Par défaut | Description |
|---|---|---|---|
| name Requis | string | - | Le nom de l'API (requis). Utilisé pour générer les noms de classes et les chemins de fichiers. |
| framework | trpc | smithy | trpc | Le framework d'API à utiliser. |
| namespace | string | - | L'espace de noms pour l'API Smithy (applicable uniquement pour le framework smithy). Par défaut, correspond à la portée de votre monorepo |
| integrationPattern | isolated | shared | isolated | Comment les intégrations API Gateway sont générées pour l'API. Choisissez entre isolated (par défaut) et shared. |
| auth | iam | cognito | custom | iam | La méthode utilisée pour s'authentifier auprès de votre API. Choisissez entre iam (par défaut), cognito ou custom. |
| directory | string | packages | Le répertoire dans lequel stocker l'application. |
| subDirectory | string | - | Le sous-répertoire dans lequel le projet est placé. Par défaut, il s'agit du nom du projet. |
| iac | inherit | cdk | terraform | inherit | Le fournisseur IaC préféré. Par défaut, celui-ci est hérité de votre sélection initiale. |
| infra | rest-lambda | http-lambda | none | rest-lambda | Le type d'infrastructure à utiliser pour déployer cette API. |
| preferInstallDependencies | boolean | true | Indique 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ée deux projets liés dans le répertoire <directory>/<api-name> :
Répertoiremodel/ Projet de modèle Smithy
- package.json Manifeste du projet définissant le nom du package et les dépendances
- project.json Configuration du projet et cibles de build
- smithy-build.json Configuration de build Smithy
- ssdk.rolldown.config.mjs Bundle le TypeScript Server SDK généré
Répertoiresrc/
- main.smithy Définition principale du service
Répertoireoperations/
- echo.smithy Exemple de définition d’opération
Répertoirebackend/ Implémentation backend TypeScript
- project.json Configuration du projet et cibles de build
- rolldown.config.ts Configuration du bundle
Répertoiresrc/
- handler.ts Gestionnaire AWS Lambda
- local-server.ts Serveur de développement local
- service.ts Implémentation du service
- context.ts Définition du contexte du service
Répertoireoperations/
- echo.ts Exemple d’implémentation d’opération
Répertoiregenerated/ SDK TypeScript généré (créé pendant le build)
- …
Infrastructure
Section intitulée « Infrastructure »Puisque ce générateur crée une 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épertoireapis/
- <project-name>.ts Construct CDK pour déployer votre API
Répertoirecore/ Constructs génériques réutilisés par les constructs dans
appRépertoireapi/
- rest-api.ts Construct CDK pour déployer une API REST
- utils.ts Utilitaires pour les constructs d’API
- index.ts Point d’entrée exportant les constructs 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épertoireapis/
Répertoire<project-name>/
- <project-name>.tf Module pour déployer votre API
Répertoirecore/ Modules génériques réutilisés par les modules dans
appRépertoireapi/
Répertoirerest-api/
- rest-api.tf Module pour déployer une API REST
- project.json Cibles de build et configuration du projet
Architecture
Section intitulée « Architecture »L’API Smithy déployée a l’architecture suivante, avec une Web ACL AWS WAFv2 devant l’étape API Gateway :
Implémenter votre API Smithy
Section intitulée « Implémenter votre API Smithy »Définir des opérations dans Smithy
Section intitulée « Définir des opérations dans Smithy »Les opérations sont définies dans des fichiers Smithy au sein du projet de modèle. La définition principale du service se trouve dans main.smithy :
$version: "2.0"
namespace your.namespace
use aws.protocols#restJson1use smithy.framework#ValidationException
@title("YourService")@restJson1service YourService { version: "1.0.0" operations: [ Echo, // Add your operations here ] errors: [ ValidationException ]}Les opérations individuelles sont définies dans des fichiers séparés dans le répertoire operations/ :
$version: "2.0"
namespace your.namespace
@http(method: "POST", uri: "/echo")operation Echo { input: EchoInput output: EchoOutput}
structure EchoInput { @required message: String
foo: Integer bar: String}
structure EchoOutput { @required message: String}Ajout d’une bibliothèque de formes
Section intitulée « Ajout d’une bibliothèque de formes »Si vous avez plusieurs API Smithy qui partagent les mêmes types de données, vous pouvez définir ces types une fois dans une bibliothèque de formes plutôt que de les dupliquer dans chaque modèle. Une bibliothèque de formes est un projet Smithy sans service — juste des formes réutilisables — dont n’importe quel nombre de projets Smithy peuvent dépendre.
Générez-en une avec le générateur smithy#project :
pnpm nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapesyarn nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapesnpx nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapesbunx nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapesVous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
pnpm nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapes --dry-runyarn nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapes --dry-runnpx nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapes --dry-runbunx nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapes --dry-run- 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 - smithy#project - Remplissez les paramètres requis
- name: my-shapes
- type: shapes
- Cliquez sur
Generate
Le modèle de votre API peut ensuite référencer ses formes avec use :
$version: "2.0"
namespace com.example.api
use com.example.shared#Customer
structure GetCustomerOutput { @required customer: Customer}Consultez le guide du projet Smithy pour savoir comment créer une bibliothèque de formes et la configurer comme dépendance du modèle de votre API.
Implémenter des opérations en TypeScript
Section intitulée « Implémenter des opérations en TypeScript »Les implémentations d’opérations sont situées dans le répertoire src/operations/ du projet backend. Chaque opération est implémentée en utilisant les types générés à partir du TypeScript Server SDK (généré au moment du build à partir de votre modèle Smithy).
import { ServiceContext } from '../context.js';import { Echo as EchoOperation } from '../generated/ssdk/index.js';
export const Echo: EchoOperation<ServiceContext> = async (input) => { // Your business logic here return { message: `Echo: ${input.message}` // type-safe based on your Smithy model };};Les opérations doivent être enregistrées dans la définition du service dans src/service.ts :
import { ServiceContext } from './context.js';import { YourServiceService } from './generated/ssdk/index.js';import { Echo } from './operations/echo.js';// Import other operations here
// Register operations to the service hereexport const Service: YourServiceService<ServiceContext> = { Echo, // Add other operations here};Contexte du service
Section intitulée « Contexte du service »Vous pouvez définir un contexte partagé pour vos opérations dans context.ts :
export interface ServiceContext { // Powertools tracer, logger and metrics are provided by default tracer: Tracer; logger: Logger; metrics: Metrics; // Add shared dependencies, database connections, etc. dbClient: any; userIdentity: string;}Ce contexte est passé à toutes les implémentations d’opérations et peut être utilisé pour partager des ressources comme des connexions de base de données, de la configuration ou des utilitaires de journalisation.
Observabilité avec AWS Lambda Powertools
Section intitulée « Observabilité avec AWS Lambda Powertools »Journalisation
Section intitulée « Journalisation »Le générateur configure la journalisation structurée en utilisant AWS Lambda Powertools avec injection automatique de contexte via le middleware Middy.
export const handler = middy<APIGatewayProxyEvent, APIGatewayProxyResult>() .use(captureLambdaHandler(tracer)) .use(injectLambdaContext(logger)) .use(logMetrics(metrics)) .handler(lambdaHandler);Vous pouvez accéder au logger depuis vos implémentations d’opérations via le contexte :
import { ServiceContext } from '../context.js';import { Echo as EchoOperation } from '../generated/ssdk/index.js';
export const Echo: EchoOperation<ServiceContext> = async (input, ctx) => { ctx.logger.info('Your log message'); // ...};Le traçage AWS X-Ray est configuré automatiquement via le middleware captureLambdaHandler.
export const handler = middy<APIGatewayProxyEvent, APIGatewayProxyResult>() .use(captureLambdaHandler(tracer)) .use(injectLambdaContext(logger)) .use(logMetrics(metrics)) .handler(lambdaHandler);Vous pouvez ajouter des sous-segments personnalisés à vos traces dans vos opérations :
import { ServiceContext } from '../context.js';import { Echo as EchoOperation } from '../generated/ssdk/index.js';
export const Echo: EchoOperation<ServiceContext> = async (input, ctx) => { // Creates a new subsegment const subsegment = ctx.tracer.getSegment()?.addNewSubsegment('custom-operation'); try { // Your logic here } catch (error) { subsegment?.addError(error as Error); throw error; } finally { subsegment?.close(); }};Métriques
Section intitulée « Métriques »Les métriques CloudWatch sont collectées automatiquement pour chaque requête via le middleware logMetrics.
export const handler = middy<APIGatewayProxyEvent, APIGatewayProxyResult>() .use(captureLambdaHandler(tracer)) .use(injectLambdaContext(logger)) .use(logMetrics(metrics)) .handler(lambdaHandler);Vous pouvez ajouter des métriques personnalisées dans vos opérations :
import { MetricUnit } from '@aws-lambda-powertools/metrics';import { ServiceContext } from '../context.js';import { Echo as EchoOperation } from '../generated/ssdk/index.js';
export const Echo: EchoOperation<ServiceContext> = async (input, ctx) => { ctx.metrics.addMetric("CustomMetric", MetricUnit.Count, 1); // ...};Gestion des erreurs
Section intitulée « Gestion des erreurs »Smithy fournit une gestion des erreurs intégrée. Vous pouvez définir des erreurs personnalisées dans votre modèle Smithy :
@error("client")@httpError(400)structure InvalidRequestError { @required message: String}Et les enregistrer dans votre opération/service :
operation MyOperation { ... errors: [InvalidRequestError]}Puis les lever dans votre implémentation TypeScript :
import { InvalidRequestError } from '../generated/ssdk/index.js';
export const MyOperation: MyOperationHandler<ServiceContext> = async (input) => { if (!input.requiredField) { throw new InvalidRequestError({ message: "Required field is missing" }); }
return { /* success response */ };};Accéder à l’utilisateur appelant
Section intitulée « Accéder à l’utilisateur appelant »Lorsque votre API est protégée par authentification, vos opérations ont souvent besoin de savoir qui appelle. L’approche recommandée est de résoudre l’identité de l’appelant une fois dans le gestionnaire et de la transmettre via le contexte du service pour consommation par des opérations spécifiques.
Nous modéliserons le cas non autorisé comme une erreur Smithy afin qu’elle se sérialise en une réponse 403 appropriée. Ajoutez-la à votre modèle, par exemple dans model/src/operations/errors.smithy, et référencez-la sur toute opération qui nécessite une identité :
$version: "2.0"
namespace your.namespace
/// Thrown when the calling user cannot be determined@error("client")@httpError(403)structure UnauthorizedError { @required message: String}Tout d’abord, exposez l’identité résolue sur le contexte du service dans src/context.ts. Nous la fournissons en tant que fonction afin que l’UnauthorizedError soit levée depuis une opération (où le Server SDK la sérialise en un 403), plutôt que depuis le gestionnaire :
import { Logger } from '@aws-lambda-powertools/logger';import { Metrics } from '@aws-lambda-powertools/metrics';import { Tracer } from '@aws-lambda-powertools/tracer';
export interface Identity { sub: string; username: string;}
/** * Context provided to all operations. */export interface ServiceContext { tracer: Tracer; logger: Logger; metrics: Metrics; getIdentity: () => Promise<Identity>;}Ensuite, écrivez le résolveur dans src/identity.ts. Il lève UnauthorizedError lorsque l’appelant ne peut pas être déterminé. L’implémentation dépend de votre méthode auth sélectionnée :
Pour l’authentification IAM, nous recherchons l’appelant dans Cognito en utilisant le sub extrait de l’événement API Gateway :
import { CognitoIdentityProvider } from '@aws-sdk/client-cognito-identity-provider';import type { APIGatewayProxyEvent } from 'aws-lambda';import { Identity } from './context.js';import { UnauthorizedError } from './generated/ssdk/index.js';
const cognito = new CognitoIdentityProvider();
export const getIdentity = async ( event: APIGatewayProxyEvent,): Promise<Identity> => { const cognitoAuthenticationProvider = event.requestContext?.identity?.cognitoAuthenticationProvider;
let sub: string | undefined = undefined; if (cognitoAuthenticationProvider) { const providerParts = cognitoAuthenticationProvider.split(':'); sub = providerParts[providerParts.length - 1]; }
if (!sub) { throw new UnauthorizedError({ 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 UnauthorizedError({ message: `No user found with subjectId ${sub}` }); }
return { sub, username: Users[0].Username! };};Avec auth: 'cognito', l’autorisateur Cognito User Pools 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 à event.requestContext.authorizer.claims :
import type { APIGatewayProxyEvent } from 'aws-lambda';import { Identity } from './context.js';import { UnauthorizedError } from './generated/ssdk/index.js';
export const getIdentity = async ( event: APIGatewayProxyEvent,): Promise<Identity> => { const claims = event.requestContext?.authorizer?.claims as | Record<string, string> | undefined;
const sub = claims?.sub; const username = claims?.username;
if (!sub || !username) { throw new UnauthorizedError({ message: 'Unable to determine calling user' }); }
return { sub, username };};Ensuite, connectez le résolveur au contexte dans src/handler.ts :
import { Service } from './service.js';import { getIdentity } from './identity.js';// ...const httpResponse = await serviceHandler.handle(httpRequest, { tracer, logger, metrics, getIdentity: () => getIdentity(event),});Nous pouvons maintenant utiliser l’identité résolue dans une opération, par exemple dans src/operations/echo.ts :
import { ServiceContext } from '../context.js';import { Echo as EchoOperation } from '../generated/ssdk/index.js';
export const Echo: EchoOperation<ServiceContext> = async (input, ctx) => { const identity = await ctx.getIdentity(); return { message: `${identity.username} says ${input.message}` };};Build et génération de code
Section intitulée « Build et génération de code »Le projet de modèle Smithy utilise la CLI Smithy pour construire les artefacts Smithy et générer le TypeScript Server SDK :
pnpm nx build <model-project>yarn nx build <model-project>npx nx build <model-project>bunx nx build <model-project>Sur macOS et Linux, la CLI est résolue par mise, que le build récupère à la demande, donc il n’y a rien à installer — elle télécharge et met en cache la version épinglée la première fois que vous construisez.
Ce processus :
- Compile le modèle Smithy et le valide
- Génère la spécification OpenAPI à partir du modèle Smithy
- Crée le TypeScript Server SDK avec des interfaces d’opération type-safe
- Produit les artefacts de build dans
dist/<model-project>/build/
Le projet backend copie automatiquement le SDK généré pendant la compilation :
pnpm nx copy-ssdk <backend-project>yarn nx copy-ssdk <backend-project>npx nx copy-ssdk <backend-project>bunx nx copy-ssdk <backend-project>Build sur Windows
Section intitulée « Build sur Windows »mise ne publie aucun package Windows sur npm, donc sur Windows la CLI Smithy est un prérequis que vous installez vous-même. Installez-la une fois en suivant le guide d’installation de la CLI Smithy (par exemple winget install smithy ou scoop install smithy), et assurez-vous que smithy est dans votre PATH. Un projet Smithy généré sur Windows exécute smithy directement plutôt que via mise.
Alternativement, développez dans WSL, où le build exécute le chemin Linux et mise résout la CLI pour vous — rien à installer.
Un projet généré sur Windows commit une cible compile qui invoque smithy directement, donc toute autre personne travaillant dessus — y compris sur macOS ou Linux — a besoin de la CLI Smithy dans son PATH également. Pour que ces machines résolvent la CLI via mise à la place, basculez la cible vers la commande mise comme décrit ci-dessous.
Choisir comment la CLI est résolue
Section intitulée « Choisir comment la CLI est résolue »macOS et Linux résolvent la CLI via mise et Windows utilise une CLI installée globalement, mais vous pouvez choisir l’une ou l’autre sur n’importe quelle plateforme en modifiant la commande de la cible compile dans le project.json du projet de modèle.
Pour utiliser une CLI Smithy installée globalement au lieu de mise, remplacez le préfixe mise par un simple smithy :
{ "targets": { "compile": { "options": { "commands": ["... npx -y mise@<version> exec smithy@<version> -- smithy build ..."] "commands": ["... smithy build ..."] } } }}Pour revenir à mise résolvant la CLI, restaurez le préfixe npx -y mise@<version> exec smithy@<version> --.
Cible Bundle
Section intitulée « Cible 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.
Développement local
Section intitulée « Développement local »Le générateur configure un serveur de développement local avec rechargement à chaud :
pnpm nx serve <backend-project>yarn nx serve <backend-project>npx nx serve <backend-project>bunx nx serve <backend-project>Déployer votre API Smithy
Section intitulée « Déployer votre API Smithy »Le générateur crée une infrastructure CDK ou Terraform basée sur votre iac sélectionné.
Le construct CDK pour déployer votre API se trouve dans le dossier common/constructs :
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(), }); }}Cela configure :
- Une fonction AWS Lambda pour le service Smithy
- Une API REST API Gateway comme déclencheur de fonction
- Des rôles IAM et des permissions
- Un groupe de logs CloudWatch
- Une configuration de traçage X-Ray
Les modules Terraform pour déployer votre API se trouvent dans le dossier common/terraform.
Le module API stocke son zip de déploiement Lambda dans un bucket S3 d’actifs partagé — consultez le guide d’infrastructure Terraform pour plus de détails. Instanciez le module core/asset-bucket une fois par déploiement et transmettez 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}Cela configure :
- Une fonction AWS Lambda qui sert l’API Smithy
- Une API REST API Gateway comme déclencheur de fonction
- Des rôles IAM et des permissions
- Un groupe de logs CloudWatch
- Une configuration de traçage X-Ray
- Une configuration CORS
Le module Terraform fournit plusieurs sorties :
# 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}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.
Les constructs CDK fournissent un support complet d’intégration type-safe 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 de routeur avec une seule fonction Lambda. 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
# The module automatically creates a single Lambda function # that handles all API operations 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 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');Avec le modèle de routeur de Terraform, il n’y a qu’une seule fonction Lambda. Vous pouvez y accéder via les sorties du module :
# Grant additional permissions to the single Lambda functionresource "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 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 la fonction Lambda dans votre VPC derrière un groupe de sécurité 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é.
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.
Pour personnaliser les options pour des opérations spécifiques avec Terraform, vous devez modifier le module Terraform généré pour configurer des fonctions Lambda individuelles par opération (voir la section Intégrations explicites ci-dessous).
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(...);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(),});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(...), }, },});Pour des intégrations explicites par opération avec Terraform, vous devez modifier le module spécifique à l’application généré pour remplacer l’intégration proxy par défaut par des intégrations spécifiques pour chaque opération.
Modifiez packages/common/terraform/src/app/apis/my-api/my-api.tf :
- Supprimez les routes proxy par défaut (par exemple,
resource "aws_apigatewayv2_route" "proxy_routes") - Remplacez la fonction Lambda unique par des fonctions individuelles pour chaque opération
- Créez des intégrations et routes spécifiques pour chaque opération, en réutilisant le même bundle ZIP :
# Remove the default single Lambda function resource "aws_lambda_function" "api_lambda" { s3_bucket = aws_s3_object.lambda_zip.bucket s3_key = aws_s3_object.lambda_zip.key s3_object_version = aws_s3_object.lambda_zip.version_id function_name = "MyApiHandler" role = aws_iam_role.lambda_execution_role.arn handler = "index.handler" runtime = "nodejs22.x" timeout = 30 # ... rest of configuration }
# Remove the default proxy integration 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 # ... rest of configuration }
# Remove the default proxy routes 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}" # ... rest of configuration }
# Add individual Lambda functions for each operation using the same bundle resource "aws_lambda_function" "say_hello_handler" { s3_bucket = aws_s3_object.lambda_zip.bucket s3_key = aws_s3_object.lambda_zip.key s3_object_version = aws_s3_object.lambda_zip.version_id function_name = "MyApi-SayHello" role = aws_iam_role.lambda_execution_role.arn handler = "sayHello.handler" # Specific handler for this operation runtime = "nodejs22.x" timeout = 30 source_code_hash = data.archive_file.lambda_zip.output_base64sha256
tracing_config { mode = "Active" }
environment { variables = var.env }
tags = var.tags }
resource "aws_lambda_function" "get_documentation_handler" { s3_bucket = aws_s3_object.lambda_zip.bucket s3_key = aws_s3_object.lambda_zip.key s3_object_version = aws_s3_object.lambda_zip.version_id function_name = "MyApi-GetDocumentation" role = aws_iam_role.lambda_execution_role.arn handler = "getDocumentation.handler" # Specific handler for this operation runtime = "nodejs22.x" timeout = 30 source_code_hash = data.archive_file.lambda_zip.output_base64sha256
tracing_config { mode = "Active" }
environment { variables = var.env }
tags = var.tags }
# Add specific integrations for each operation 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" }
# Add specific routes for each operation 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" }
# Add Lambda permissions for each function 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}/*/*" }# Remove the default single Lambda function resource "aws_lambda_function" "api_lambda" { s3_bucket = aws_s3_object.lambda_zip.bucket s3_key = aws_s3_object.lambda_zip.key s3_object_version = aws_s3_object.lambda_zip.version_id function_name = "MyApiHandler-${random_string.suffix.result}" role = aws_iam_role.lambda_execution_role.arn handler = "index.handler" runtime = "nodejs22.x" timeout = 30 # ... rest of configuration }
# Remove the default proxy integration resource "aws_api_gateway_integration" "lambda_integration" { rest_api_id = module.rest_api.api_id resource_id = aws_api_gateway_resource.proxy_resource.id http_method = aws_api_gateway_method.proxy_method.http_method integration_http_method = "POST" type = "AWS_PROXY" uri = aws_lambda_function.api_lambda.invoke_arn # ... rest of configuration }
# Remove the default catch-all proxy method resource "aws_api_gateway_method" "proxy_method" { rest_api_id = module.rest_api.api_id resource_id = aws_api_gateway_resource.proxy_resource.id http_method = "ANY" # ... rest of configuration }
# Add individual Lambda functions for each operation using the same bundle resource "aws_lambda_function" "say_hello_handler" { s3_bucket = aws_s3_object.lambda_zip.bucket s3_key = aws_s3_object.lambda_zip.key s3_object_version = aws_s3_object.lambda_zip.version_id function_name = "MyApi-SayHello" role = aws_iam_role.lambda_execution_role.arn handler = "sayHello.handler" # Specific handler for this operation runtime = "nodejs22.x" timeout = 30 source_code_hash = data.archive_file.lambda_zip.output_base64sha256
tracing_config { mode = "Active" }
environment { variables = var.env }
tags = var.tags }
resource "aws_lambda_function" "get_documentation_handler" { s3_bucket = aws_s3_object.lambda_zip.bucket s3_key = aws_s3_object.lambda_zip.key s3_object_version = aws_s3_object.lambda_zip.version_id function_name = "MyApi-GetDocumentation" role = aws_iam_role.lambda_execution_role.arn handler = "getDocumentation.handler" # Specific handler for this operation runtime = "nodejs22.x" timeout = 30 source_code_hash = data.archive_file.lambda_zip.output_base64sha256
tracing_config { mode = "Active" }
environment { variables = var.env }
tags = var.tags }
# Add specific resources and methods for each operation 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" }
# Update deployment to depend on new integrations~ 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, ])) } }
# Add Lambda permissions for each function 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 d’intégration
Section intitulée « Modèle d’intégration »Les constructs d’API CDK générés prennent en charge deux modèles d’intégration :
isolatedcrée une fonction Lambda par opération. C’est le modèle par défaut pour les API générées.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. shared réduit la prolifération des fonctions Lambda et des intégrations API Gateway tout en permettant des remplacements sélectifs.
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', ... }); };}Les modules Terraform utilisent automatiquement le modèle de routeur - c’est l’approche par défaut et la seule prise en charge. Le module généré crée une seule fonction Lambda qui gère toutes les opérations API.
Vous pouvez simplement instancier le module par défaut pour obtenir le modèle de routeur :
# Default router pattern - single Lambda function for all operationsmodule "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# Single Lambda function handles all operations automatically tags = local.common_tags}Génération de code
Section intitulée « Génération de code »Puisque les opérations sont définies dans Smithy, nous utilisons la génération de code pour fournir des métadonnées au construct CDK pour des intégrations type-safe.
Une cible generate:<ApiName>-metadata est ajoutée au project.json des constructs communs pour faciliter cette génération de code, qui émet un fichier tel que packages/common/constructs/src/generated/my-api/metadata.gen.ts. Puisque cela est généré au moment du build, il est ignoré dans le contrôle de version.
Accorder l’accès (IAM uniquement)
Section intitulée « Accorder l’accès (IAM uniquement) »Si vous avez sélectionné l’authentification IAM, vous pouvez utiliser la méthode grantInvokeAccess pour accorder l’accès à votre API :
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 Smithy 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 roleresource "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}Invoquer votre API Smithy
Section intitulée « Invoquer votre API Smithy »Pour invoquer votre API depuis un site web React, vous pouvez utiliser le générateur connection, qui fournit une génération de client type-safe à partir de votre modèle Smithy.
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 :