Smithy TypeScript API
Smithy è un linguaggio di definizione di interfacce indipendente dal protocollo per creare API in modo modellato.
Il generatore di API Smithy TypeScript crea una nuova API utilizzando Smithy per la definizione del servizio e lo Smithy TypeScript Server SDK per l’implementazione. Il generatore fornisce infrastruttura come codice CDK o Terraform per distribuire il servizio su AWS Lambda, esposto tramite un’API REST AWS API Gateway. Offre sviluppo API type-safe con generazione automatica del codice dai modelli Smithy. L’handler generato utilizza AWS Lambda Powertools for TypeScript per l’osservabilità, inclusi logging, tracciamento AWS X-Ray e metriche CloudWatch.
Utilizzo
Sezione intitolata “Utilizzo”Genera un’API Smithy TypeScript
Sezione intitolata “Genera un’API Smithy TypeScript”Puoi generare una nuova API Smithy TypeScript in due modi:
- Installa il Nx Console VSCode Plugin se non l'hai già fatto
- Apri la console Nx in VSCode
- Clicca su
Generate (UI)nella sezione "Common Nx Commands" - Cerca
@aws/nx-plugin - ts#api - Compila i parametri richiesti
- framework: smithy
- Clicca su
Generate
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=smithyPuoi anche eseguire una prova per vedere quali file verrebbero modificati
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-runOpzioni
Sezione intitolata “Opzioni”| Parametro | Tipo | Predefinito | Descrizione |
|---|---|---|---|
| name Obbligatorio | string | - | Il nome dell'API (obbligatorio). Utilizzato per generare i nomi delle classi e i percorsi dei file. |
| framework | trpc | smithy | trpc | Il framework API da utilizzare. |
| namespace | string | - | Il namespace per l'API Smithy (applicabile solo per il framework smithy). Il valore predefinito è lo scope del monorepo |
| integrationPattern | isolated | shared | isolated | Come vengono generate le integrazioni API Gateway per l'API. Scegli tra isolated (predefinito) e shared. |
| auth | iam | cognito | custom | iam | Il metodo utilizzato per autenticare con la tua API. Scegli tra iam (predefinito), cognito o custom. |
| directory | string | packages | La directory in cui memorizzare l'applicazione. |
| subDirectory | string | - | La sottodirectory in cui viene posizionato il progetto. Per impostazione predefinita corrisponde al nome del progetto. |
| iac | inherit | cdk | terraform | inherit | Il provider IaC preferito. Per impostazione predefinita viene ereditato dalla selezione iniziale. |
| infra | rest-lambda | http-lambda | none | rest-lambda | Il tipo di infrastruttura da utilizzare per distribuire questa API. |
| preferInstallDependencies | boolean | true | Se preferire l'installazione delle dipendenze dopo l'esecuzione del generatore. Impostare su false per rimandare l'installazione quando si eseguono più generatori in batch (l'installazione viene comunque eseguita se necessaria affinché i generatori successivi possano calcolare il grafo dei progetti Nx); installare una volta alla fine. |
Output del Generatore
Sezione intitolata “Output del Generatore”Il generatore crea due progetti correlati nella directory <directory>/<api-name>:
Directorymodel/ Progetto del modello Smithy
- project.json Configurazione del progetto e target di build
- smithy-build.json Configurazione di build Smithy
- build.Dockerfile Configurazione Docker per la creazione di artefatti Smithy
Directorysrc/
- main.smithy Definizione principale del servizio
Directoryoperations/
- echo.smithy Definizione di esempio di un’operazione
Directorybackend/ Implementazione TypeScript del backend
- project.json Configurazione del progetto e target di build
- rolldown.config.ts Configurazione del bundle
Directorysrc/
- handler.ts Handler AWS Lambda
- local-server.ts Server di sviluppo locale
- service.ts Implementazione del servizio
- context.ts Definizione del contesto del servizio
Directoryoperations/
- echo.ts Implementazione di esempio di un’operazione
Directorygenerated/ SDK TypeScript generato (creato durante la build)
- …
Infrastruttura
Sezione intitolata “Infrastruttura”Poiché questo generatore crea infrastruttura come codice in base al iacProvider scelto, genererà un progetto in packages/common che include i costrutti CDK o i moduli Terraform rilevanti.
Il progetto comune di infrastruttura come codice è strutturato come segue:
Directorypackages/common/constructs
Directorysrc
Directoryapp/ Costrutti per infrastruttura specifica di un progetto/generatore
Directoryapis/
- <project-name>.ts Costrutto CDK per distribuire la tua API
Directorycore/ Costrutti generici riutilizzati da quelli in
appDirectoryapi/
- rest-api.ts Costrutto CDK per distribuire un’API REST
- utils.ts Utility per i costrutti API
- index.ts Punto di ingresso che esporta i costrutti da
app
- project.json Target di build e configurazione del progetto
Directorypackages/common/terraform
Directorysrc
Directoryapp/ Moduli Terraform per infrastruttura specifica di un progetto/generatore
Directoryapis/
Directory<project-name>/
- <project-name>.tf Modulo per distribuire la tua API
Directorycore/ Moduli generici riutilizzati da quelli in
appDirectoryapi/
Directoryrest-api/
- rest-api.tf Modulo per distribuire un’API REST
- project.json Target di build e configurazione del progetto
Architettura
Sezione intitolata “Architettura”L’API Smithy distribuita ha la seguente architettura, con un Web ACL AWS WAFv2 davanti allo stage API Gateway:
Implementare la tua API Smithy
Sezione intitolata “Implementare la tua API Smithy”Definire Operazioni in Smithy
Sezione intitolata “Definire Operazioni in Smithy”Le operazioni sono definite in file Smithy all’interno del progetto del modello. La definizione principale del servizio è in 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, // Aggiungi qui le tue operazioni ] errors: [ ValidationException ]}Le singole operazioni sono definite in file separati nella directory 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}Implementare Operazioni in TypeScript
Sezione intitolata “Implementare Operazioni in TypeScript”Le implementazioni delle operazioni si trovano nella directory src/operations/ del progetto backend. Ogni operazione è implementata utilizzando i tipi generati dall’SDK Server TypeScript (generati al momento della build dal tuo modello Smithy).
import { ServiceContext } from '../context.js';import { Echo as EchoOperation } from '../generated/ssdk/index.js';
export const Echo: EchoOperation<ServiceContext> = async (input) => { // La tua business logic qui return { message: `Echo: ${input.message}` // type-safe basato sul modello Smithy };};Le operazioni devono essere registrate nella definizione del servizio in src/service.ts:
import { ServiceContext } from './context.js';import { YourServiceService } from './generated/ssdk/index.js';import { Echo } from './operations/echo.js';// Importa altre operazioni qui
// Registra le operazioni al servizio quiexport const Service: YourServiceService<ServiceContext> = { Echo, // Aggiungi altre operazioni qui};Contesto del Servizio
Sezione intitolata “Contesto del Servizio”Puoi definire un contesto condiviso per le tue operazioni in context.ts:
export interface ServiceContext { // Tracer, logger e metrics di Powertools sono forniti di default tracer: Tracer; logger: Logger; metrics: Metrics; // Aggiungi dipendenze condivise, connessioni al database, ecc. dbClient: any; userIdentity: string;}Questo contesto viene passato a tutte le implementazioni delle operazioni e può essere utilizzato per condividere risorse come connessioni al database, configurazioni o utility di logging.
Osservabilità con AWS Lambda Powertools
Sezione intitolata “Osservabilità con AWS Lambda Powertools”Logging
Sezione intitolata “Logging”Il generatore configura il logging strutturato utilizzando AWS Lambda Powertools con iniezione automatica del contesto tramite middleware Middy.
export const handler = middy<APIGatewayProxyEvent, APIGatewayProxyResult>() .use(captureLambdaHandler(tracer)) .use(injectLambdaContext(logger)) .use(logMetrics(metrics)) .handler(lambdaHandler);Puoi accedere al logger dalle implementazioni delle operazioni tramite il contesto:
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('Il tuo messaggio di log'); // ...};Tracciamento
Sezione intitolata “Tracciamento”Il tracciamento AWS X-Ray è configurato automaticamente tramite il middleware captureLambdaHandler.
export const handler = middy<APIGatewayProxyEvent, APIGatewayProxyResult>() .use(captureLambdaHandler(tracer)) .use(injectLambdaContext(logger)) .use(logMetrics(metrics)) .handler(lambdaHandler);Puoi aggiungere sottosegmenti personalizzati ai tuoi trace nelle operazioni:
import { ServiceContext } from '../context.js';import { Echo as EchoOperation } from '../generated/ssdk/index.js';
export const Echo: EchoOperation<ServiceContext> = async (input, ctx) => { // Crea un nuovo sottosegmento const subsegment = ctx.tracer.getSegment()?.addNewSubsegment('custom-operation'); try { // La tua logica qui } catch (error) { subsegment?.addError(error as Error); throw error; } finally { subsegment?.close(); }};Metriche
Sezione intitolata “Metriche”Le metriche CloudWatch vengono raccolte automaticamente per ogni richiesta tramite il middleware logMetrics.
export const handler = middy<APIGatewayProxyEvent, APIGatewayProxyResult>() .use(captureLambdaHandler(tracer)) .use(injectLambdaContext(logger)) .use(logMetrics(metrics)) .handler(lambdaHandler);Puoi aggiungere metriche personalizzate nelle tue operazioni:
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); // ...};Gestione degli Errori
Sezione intitolata “Gestione degli Errori”Smithy fornisce una gestione degli errori integrata. Puoi definire errori personalizzati nel tuo modello Smithy:
@error("client")@httpError(400)structure InvalidRequestError { @required message: String}E registrarli alla tua operazione/servizio:
operation MyOperation { ... errors: [InvalidRequestError]}Poi lanciali nella tua implementazione TypeScript:
import { InvalidRequestError } from '../generated/ssdk/index.js';
export const MyOperation: MyOperationHandler<ServiceContext> = async (input) => { if (!input.requiredField) { throw new InvalidRequestError({ message: "Il campo obbligatorio è mancante" }); }
return { /* risposta di successo */ };};Accesso all’Utente Chiamante
Sezione intitolata “Accesso all’Utente Chiamante”Quando la tua API è protetta da autenticazione, le tue operazioni spesso devono sapere chi sta chiamando. L’approccio più pulito è risolvere l’identità del chiamante una volta nell’handler e passarla attraverso il contesto del servizio, in modo che le tue operazioni rimangano libere da qualsiasi parsing dell’evento di API Gateway.
Come esempio, aggiungiamo un’operazione Me che restituisce i dettagli sull’utente chiamante.
Prima, aggiungi l’identità risolta al contesto del servizio in src/context.ts:
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; identity?: Identity;}Successivamente, scrivi una funzione che risolve l’identità dall’evento di API Gateway in src/identity.ts. L’implementazione dipende dal metodo auth selezionato:
Per l’autenticazione IAM, cerchiamo il chiamante in Cognito usando il sub estratto dall’evento di 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! };};Con auth: 'cognito', l’autorizzatore Cognito User Pools di API Gateway verifica il JWT che il chiamante fornisce nell’header Authorization e posiziona i claim verificati sull’evento in event.requestContext.authorizer.claims. Leggiamo semplicemente quei claim — nessuna chiamata extra all’SDK AWS, nessuna verifica manuale del JWT.
Il generatore collega l’autorizzatore per accettare token di accesso Cognito, quindi username è il claim del nome utente canonico; ricadiamo su cognito:username nel caso tu riconfiguri l’autorizzatore per accettare token ID:
import type { APIGatewayProxyEvent } from 'aws-lambda';import { Identity } from './context.js';
export const getIdentity = ( event: APIGatewayProxyEvent,): Identity | undefined => { const claims = event.requestContext?.authorizer?.claims as | Record<string, string> | undefined;
const sub = claims?.sub; const username = claims?.username ?? claims?.['cognito:username'];
if (!sub || !username) { return undefined; }
return { sub, username };};Quindi chiama getIdentity in src/handler.ts e aggiungi il risultato al contesto:
import { Service } from './service.js';import { getIdentity } from './identity.js';// ...const httpResponse = await serviceHandler.handle(httpRequest, { tracer, logger, metrics, identity: await getIdentity(event),});Ora definisci l’operazione Me nel tuo modello Smithy, ad esempio in model/src/operations/me.smithy:
$version: "2.0"
namespace your.namespace
/// Return details about the calling user@http(method: "GET", uri: "/me")@readonlyoperation Me { output := { @required sub: String
@required username: String } errors: [ UnauthorizedError ]}
/// Thrown when the calling user cannot be determined@error("client")@httpError(403)structure UnauthorizedError { @required message: String}Ricorda di registrare l’operazione sul tuo servizio in model/src/main.smithy:
service YourService { operations: [ Echo Me ]}Implementa l’operazione in src/operations/me.ts, leggendo l’identità risolta direttamente dal contesto:
import { ServiceContext } from '../context.js';import { Me as MeOperation, UnauthorizedError,} from '../generated/ssdk/index.js';
export const Me: MeOperation<ServiceContext> = async (_input, ctx) => { if (!ctx.identity) { throw new UnauthorizedError({ message: 'Unable to determine calling user' }); }
return { sub: ctx.identity.sub, username: ctx.identity.username };};Infine, registra l’operazione sul tuo servizio in src/service.ts:
import { ServiceContext } from './context.js';import { YourServiceService } from './generated/ssdk/index.js';import { Echo } from './operations/echo.js';import { Me } from './operations/me.js';
// Register operations to the service hereexport const Service: YourServiceService<ServiceContext> = { Echo, Me,};Build e Generazione del Codice
Sezione intitolata “Build e Generazione del Codice”Il progetto del modello Smithy utilizza Docker per costruire gli artefatti Smithy e generare l’SDK Server TypeScript:
pnpm nx build <model-project>yarn nx build <model-project>npx nx build <model-project>bunx nx build <model-project>Questo processo:
- Compila il modello Smithy e lo valida
- Genera la specifica OpenAPI dal modello Smithy
- Crea l’SDK Server TypeScript con interfacce type-safe per le operazioni
- Produce artefatti di build in
dist/<model-project>/build/
Il progetto backend copia automaticamente l’SDK generato durante la compilazione:
pnpm nx copy-ssdk <backend-project>yarn nx copy-ssdk <backend-project>npx nx copy-ssdk <backend-project>bunx nx copy-ssdk <backend-project>Target di Bundle
Sezione intitolata “Target di Bundle”Il generatore configura automaticamente un target bundle che utilizza Rolldown per creare un pacchetto di distribuzione:
pnpm nx bundle <project-name>yarn nx bundle <project-name>npx nx bundle <project-name>bunx nx bundle <project-name>La configurazione di Rolldown si trova in rolldown.config.ts, con un’entry per bundle da generare. Rolldown gestisce la creazione di più bundle in parallelo se definiti.
Sviluppo Locale
Sezione intitolata “Sviluppo Locale”Il generatore configura un server di sviluppo locale con hot reloading:
pnpm nx serve <backend-project>yarn nx serve <backend-project>npx nx serve <backend-project>bunx nx serve <backend-project>Distribuzione della tua API Smithy
Sezione intitolata “Distribuzione della tua API Smithy”Il generatore crea infrastruttura CDK o Terraform in base al iacProvider selezionato.
Il costrutto CDK per distribuire la tua API si trova nella cartella common/constructs:
import { MyApi } from ':my-scope/common-constructs';
export class ExampleStack extends Stack { constructor(scope: Construct, id: string) { // Aggiungi l'API allo stack const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), }); }}Questo configura:
- Una funzione AWS Lambda per il servizio Smithy
- API Gateway REST API come trigger della funzione
- Ruoli e permessi IAM
- Log group CloudWatch
- Configurazione del tracciamento X-Ray
I moduli Terraform per distribuire la tua API si trovano nella cartella common/terraform.
Il modulo API prepara il suo zip di distribuzione Lambda in un bucket S3 di asset condiviso — vedi la guida all’infrastruttura Terraform per i dettagli. Istanzia il modulo core/asset-bucket una volta per distribuzione e passa il suo output bucket_name in ogni modulo API / Lambda tramite l’input 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
# Variabili d'ambiente per la funzione Lambda env = { ENVIRONMENT = var.environment LOG_LEVEL = "INFO" }
# Politiche IAM aggiuntive se necessarie additional_iam_policy_statements = [ # Aggiungi eventuali permessi aggiuntivi richiesti dalla tua API ]
tags = local.common_tags}Questo configura:
- Una funzione AWS Lambda che serve l’API Smithy
- API Gateway REST API come trigger della funzione
- Ruoli e permessi IAM
- Log group CloudWatch
- Configurazione del tracciamento X-Ray
- Configurazione CORS
Il modulo Terraform fornisce diversi output:
# Accedi all'endpoint APIoutput "api_url" { value = module.my_api.stage_invoke_url}
# Accedi ai dettagli della funzione Lambdaoutput "lambda_function_name" { value = module.my_api.lambda_function_name}Per le API REST, il costrutto generato associa di default una Web ACL AWS WAFv2 allo stage di API Gateway. La Web ACL utilizza il ruleset predefinito gestito da AWS (AWSManagedRulesCommonRuleSet e AWSManagedRulesKnownBadInputsRuleSet), fornendo protezione contro exploit web comuni inclusa la OWASP Top 10. I log delle richieste WAF vengono scritti in un gruppo CloudWatch Logs.
Puoi modificare il costrutto rest-api generato per aggiungere, rimuovere o regolare le regole (ad esempio, per aggiungere regole basate sulla frequenza o gruppi di regole gestite aggiuntivi).
Per disattivare (ad esempio, per collegare la tua Web ACL), imposta enableWaf su false:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), enableWaf: false,});Per disattivare (ad esempio, per collegare la tua Web ACL), imposta enable_waf su false:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name enable_waf = false}Access logging
Sezione intitolata “Access logging”Per le REST API, l’infrastruttura generata abilita la registrazione degli accessi per impostazione predefinita, scrivendo una riga JSON strutturata per richiesta in un gruppo CloudWatch Logs dedicato. Il gruppo di log è crittografato con una chiave KMS gestita dal cliente e conservato per un anno.
API Gateway scrive i log di accesso utilizzando un ruolo CloudWatch Logs a livello di account. Questo ruolo è configurato nell’impostazione AWS::ApiGateway::Account, che è un singleton per regione per account — esiste un solo ruolo per ogni REST API nella regione. Per gestire questo in modo sicuro tra più stack distribuiti indipendentemente, l’infrastruttura generata:
- Crea un ruolo CloudWatch Logs condiviso e lo configura sull’account solo quando non è già impostato un ruolo funzionante, in modo che le distribuzioni non sovrascrivano mai un ruolo di proprietà di un altro stack.
- Lascia l’impostazione dell’account intatta durante la rimozione, in modo che la distruzione di uno stack non disabiliti mai la registrazione per altre REST API nella regione.
Il ruolo dell’account è gestito dal costrutto ApiGatewayAccount, un singleton con ambito stack risolto tramite ApiGatewayAccount.ensure(scope). Ogni stage della REST API dipende da esso e il ruolo è configurato da una risorsa personalizzata supportata da Lambda.
Puoi personalizzare il formato del log di accesso passando deployOptions durante la costruzione della tua API:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), deployOptions: { accessLogFormat: AccessLogFormat.clf(), },});Il ruolo dell’account è gestito dal modulo core/api/api-gateway-account, che viene istanziato dal modulo API generato. Configura l’account in modo idempotente e non viene mai reimpostato con terraform destroy.
Puoi personalizzare il formato del log di accesso modificando il blocco access_log_settings sulla risorsa aws_api_gateway_stage nel modulo API generato.
Integrazioni
Sezione intitolata “Integrazioni”I costrutti CDK per le API REST/HTTP sono configurati per fornire un’interfaccia type-safe per definire le integrazioni per ciascuna delle tue operazioni.
I costrutti CDK forniscono supporto completo per l’integrazione type-safe come descritto di seguito.
Integrazioni Predefinite
Sezione intitolata “Integrazioni Predefinite”Puoi utilizzare il metodo statico defaultIntegrations per sfruttare il pattern predefinito, che definisce una singola funzione AWS Lambda per ogni operazione:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});I moduli Terraform utilizzano automaticamente il router pattern con una singola funzione Lambda. Nessuna configurazione aggiuntiva è necessaria:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# Il modulo crea automaticamente una singola funzione Lambda # che gestisce tutte le operazioni API tags = local.common_tags}Accesso alle Integrazioni
Sezione intitolata “Accesso alle Integrazioni”Puoi accedere alle funzioni AWS Lambda sottostanti tramite la proprietà integrations del costrutto API in modo type-safe. Ad esempio, se la tua API definisce un’operazione chiamata sayHello e devi aggiungere dei permessi a questa funzione, puoi farlo come segue:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
// sayHello è tipizzato in base alle operazioni definite nella tua APIapi.integrations.sayHello.handler.addToRolePolicy(new PolicyStatement({ effect: Effect.ALLOW, actions: [...], resources: [...],}));Se la tua API utilizza il pattern shared, il router Lambda condiviso è esposto come api.integrations.$router:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
api.integrations.$router.handler.addEnvironment('LOG_LEVEL', 'DEBUG');Con il router pattern di Terraform, esiste solo una funzione Lambda. Puoi accedervi tramite gli output del modulo:
# Concedi permessi aggiuntivi alla singola funzione Lambdaresource "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/*" } ] })}Personalizzazione delle Opzioni Predefinite
Sezione intitolata “Personalizzazione delle Opzioni Predefinite”Se desideri personalizzare le opzioni utilizzate durante la creazione delle funzioni Lambda per ogni integrazione predefinita, puoi usare il metodo withDefaultOptions. Ad esempio, per far risiedere tutte le funzioni Lambda in una VPC:
const vpc = new Vpc(this, 'Vpc', ...);
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withDefaultOptions({ vpc, }) .build(),});Per personalizzare opzioni come la configurazione VPC, devi modificare il modulo Terraform generato. Ad esempio, per aggiungere il supporto VPC a tutte le funzioni Lambda:
# Aggiungi variabili VPCvariable "vpc_subnet_ids" { description = "Lista di ID subnet VPC per la funzione Lambda" type = list(string) default = []}
variable "vpc_security_group_ids" { description = "Lista di ID security group VPC per la funzione Lambda" type = list(string) default = []}
# Aggiorna la risorsa Lambda functionresource "aws_lambda_function" "api_lambda" { # ... configurazione esistente ...
# Aggiungi configurazione VPC vpc_config { subnet_ids = var.vpc_subnet_ids security_group_ids = var.vpc_security_group_ids }}Quindi utilizza il modulo con la configurazione VPC:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# Configurazione 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}Personalizzazione delle Opzioni per Operazione
Sezione intitolata “Personalizzazione delle Opzioni per Operazione”Per personalizzare le opzioni utilizzate per creare l’integrazione predefinita per operazioni specifiche (senza influenzare le altre), puoi usare il metodo withOperationOptions. Ad esempio, se desideri aumentare il timeout della funzione Lambda per una sola operazione:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOperationOptions({ sayHello: { timeout: Duration.seconds(60), }, }) .build(),});
// Le operazioni selezionate rimangono integrazioni predefinite, quindi sono ancora tipizzate di conseguenza:api.integrations.sayHello.handler.addToRolePolicy(new PolicyStatement({ ... }));Le opzioni che specifichi vengono unite alle opzioni di integrazione predefinite (e a qualsiasi opzione impostata tramite withDefaultOptions). Nota che non puoi specificare opzioni per operazioni che hai sostituito tramite withOverrides, poiché queste non utilizzano più l’integrazione predefinita.
Incontrerai un errore di tipo se la stessa operazione è targetizzata sia da withOperationOptions che da withOverrides, indipendentemente dall’ordine in cui li chiami.
Per personalizzare le opzioni per operazioni specifiche con Terraform, devi modificare il modulo Terraform generato per configurare singole funzioni Lambda per operazione (vedi la sezione Integrazioni Esplicite di seguito).
Override delle Integrazioni
Sezione intitolata “Override delle Integrazioni”Puoi anche sovrascrivere le integrazioni per operazioni specifiche usando il metodo withOverrides. Ogni override deve specificare una proprietà integration tipizzata correttamente per il costrutto di integrazione CDK appropriato per l’API HTTP o REST. Il metodo withOverrides è anch’esso type-safe. Ad esempio, se desideri sovrascrivere un’API getDocumentation per puntare alla documentazione ospitata da un sito web esterno, puoi farlo come segue:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), }, }) .build(),});Noterai anche che l’integrazione sovrascritta non ha più una proprietà handler quando vi si accede tramite api.integrations.getDocumentation.
Puoi aggiungere proprietà aggiuntive a un’integrazione che saranno anche tipizzate di conseguenza, consentendo ad altri tipi di integrazione di essere astratti ma rimanere type-safe, ad esempio se hai creato un’integrazione S3 per un’API REST e successivamente desideri fare riferimento al bucket per una particolare operazione, puoi farlo come segue:
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(),});
// Successivamente, magari in un altro file, puoi accedere alla proprietà bucket che abbiamo definito// in modo type-safeapi.integrations.getFile.bucket.grantRead(...);Override degli Authorizer
Sezione intitolata “Override degli Authorizer”Puoi anche fornire options nella tua integrazione per sovrascrivere particolari opzioni del metodo come gli authorizer, ad esempio se desideri utilizzare l’autenticazione Cognito per la tua operazione getDocumentation:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), options: { authorizer: new CognitoUserPoolsAuthorizer(...) // per REST, o HttpUserPoolAuthorizer per un'API HTTP } }, }) .build(),});Integrazioni Esplicite
Sezione intitolata “Integrazioni Esplicite”Se preferisci, puoi scegliere di non utilizzare le integrazioni predefinite e invece fornirne direttamente una per ogni operazione. Questo è utile se, ad esempio, ogni operazione deve utilizzare un tipo diverso di integrazione o desideri ricevere un errore di tipo quando aggiungi nuove operazioni:
new MyApi(this, 'MyApi', { integrations: { sayHello: { integration: new LambdaIntegration(...), }, getDocumentation: { integration: new HttpIntegration(...), }, },});Per integrazioni esplicite per operazione con Terraform, dovresti modificare il modulo specifico dell’app generato per sostituire l’integrazione proxy predefinita con integrazioni specifiche per ogni operazione.
Modifica packages/common/terraform/src/app/apis/my-api/my-api.tf:
- Rimuovi le route proxy predefinite (es.
resource "aws_apigatewayv2_route" "proxy_routes") - Sostituisci la singola funzione Lambda con funzioni individuali per ogni operazione
- Crea integrazioni e route specifiche per ogni operazione, riutilizzando lo stesso bundle ZIP:
# Rimuovi la singola funzione Lambda predefinita 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 # ... resto della configurazione }
# Rimuovi l'integrazione proxy predefinita 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 # ... resto della configurazione }
# Rimuovi le route proxy predefinite 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}" # ... resto della configurazione }
# Aggiungi funzioni Lambda individuali per ogni operazione usando lo stesso 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 specifico per questa operazione 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" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApi-GetDocumentation" role = aws_iam_role.lambda_execution_role.arn handler = "getDocumentation.handler" # Handler specifico per questa operazione 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 }
# Aggiungi integrazioni specifiche per ogni operazione 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" }
# Aggiungi route specifiche per ogni operazione 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" }
# Aggiungi permessi Lambda per ogni funzione 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}/*/*" }# Rimuovi la singola funzione Lambda predefinita 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 # ... resto della configurazione }
# Rimuovi l'integrazione proxy predefinita 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 # ... resto della configurazione }
# Rimuovi le route proxy predefinite 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}" # ... resto della configurazione }
# Aggiungi funzioni Lambda individuali per ogni operazione usando lo stesso 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 specifico per questa operazione 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" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApi-GetDocumentation" role = aws_iam_role.lambda_execution_role.arn handler = "getDocumentation.handler" # Handler specifico per questa operazione 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 }
# Aggiungi risorse e metodi specifici per ogni operazione 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" }
# Aggiorna il deployment per dipendere dalle nuove integrazioni~ 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, ])) } }
# Aggiungi permessi Lambda per ogni funzione 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}/*/*" }Pattern di Integrazione
Sezione intitolata “Pattern di Integrazione”I costrutti API CDK generati supportano due pattern di integrazione:
isolatedcrea una funzione Lambda per operazione. Questo è il pattern predefinito per le API generate.sharedcrea un singolo router Lambda predefinito e lo riutilizza per ogni operazione a meno che non si sovrascrivano integrazioni specifiche.
isolated offre permessi e configurazione più granulari per operazione. shared riduce la proliferazione di Lambda e integrazioni API Gateway pur consentendo override selettivi.
Ad esempio, impostando pattern su 'shared' si crea una singola funzione invece di una per integrazione:
export class MyApi<...> extends ... {
public static defaultIntegrations = (scope: Construct) => { ... return IntegrationBuilder.rest({ pattern: 'shared', ... }); };}I moduli Terraform utilizzano automaticamente il router pattern - questo è l’approccio predefinito e unico supportato. Il modulo generato crea una singola funzione Lambda che gestisce tutte le operazioni API.
Puoi semplicemente istanziare il modulo predefinito per ottenere il router pattern:
# Router pattern predefinito - singola funzione Lambda per tutte le operazionimodule "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# La singola funzione Lambda gestisce automaticamente tutte le operazioni tags = local.common_tags}Generazione del Codice
Sezione intitolata “Generazione del Codice”Poiché le operazioni sono definite in Smithy, utilizziamo la generazione di codice per fornire metadati al costrutto CDK per integrazioni type-safe.
Un target generate:<ApiName>-metadata è aggiunto al project.json dei costrutti comuni per facilitare questa generazione, producendo un file come packages/common/constructs/src/generated/my-api/metadata.gen.ts. Essendo generato al momento della build, questo file è ignorato dal version control.
Concessione Accesso (Solo IAM)
Sezione intitolata “Concessione Accesso (Solo IAM)”Se hai selezionato autenticazione IAM, puoi usare il metodo grantInvokeAccess per concedere accesso alla tua API:
api.grantInvokeAccess(myIdentityPool.authenticatedRole);# Crea una politica IAM per consentire l'invocazione dell'APIresource "aws_iam_policy" "api_invoke_policy" { name = "MyApiInvokePolicy" description = "Politica per consentire l'invocazione dell'API Smithy"
policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = "execute-api:Invoke" Resource = "${module.my_api.api_execution_arn}/*/*" } ] })}
# Allega la politica a un ruolo IAMresource "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}Invocare la tua API Smithy
Sezione intitolata “Invocare la tua API Smithy”Per invocare la tua API da un sito React, puoi usare il generatore connection, che fornisce generazione type-safe del client dal tuo modello Smithy..name
policy_arn = aws_iam_policy.api_invoke_policy.arn
}
</Fragment></Infrastructure></OptionFilter>
## Invocare la tua API Smithy
Per invocare la tua API da un sito React, puoi usare il generatore <Link path="guides/connection/react-smithy">`connection`</Link>, che fornisce generazione type-safe del client dal tuo modello Smithy.
## Connessioni
Usa il generatore <Link path="guides/connection">`connection`</Link> per integrare questo progetto con altri nel tuo workspace. Le seguenti connessioni coinvolgono questo progetto:
<CardGrid> <ConnectionCard title="React to Smithy API" description="Chiama un'API Smithy da un sito React" href={`/nx-plugin-for-aws/${Astro.currentLocale || 'en'}/guides/connection/react-smithy`} source="react" target="smithy" /> <ConnectionCard title="Smithy API to Relational Database" description="Collega un'API Smithy a un database relazionale Aurora" href={`/nx-plugin-for-aws/${Astro.currentLocale || 'en'}/guides/connection/smithy-rdb`} source="smithy" target="aurora" /> <ConnectionCard title="Smithy API to TypeScript DynamoDB" description="Collega un'API Smithy a una tabella DynamoDB" href={`/nx-plugin-for-aws/${Astro.currentLocale || 'en'}/guides/connection/smithy-dynamodb`} source="smithy" target="dynamodb" /></CardGrid>