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#smithy-api
- Compila i parametri richiesti
- Clicca su
Generate
pnpm nx g @aws/nx-plugin:ts#smithy-api
yarn nx g @aws/nx-plugin:ts#smithy-api
npx nx g @aws/nx-plugin:ts#smithy-api
bunx nx g @aws/nx-plugin:ts#smithy-api
Puoi anche eseguire una prova per vedere quali file verrebbero modificati
pnpm nx g @aws/nx-plugin:ts#smithy-api --dry-run
yarn nx g @aws/nx-plugin:ts#smithy-api --dry-run
npx nx g @aws/nx-plugin:ts#smithy-api --dry-run
bunx nx g @aws/nx-plugin:ts#smithy-api --dry-run
Opzioni
Sezione intitolata “Opzioni”Parametro | Tipo | Predefinito | Descrizione |
---|---|---|---|
name Obbligatorio | string | - | The name of the API (required). Used to generate class names and file paths. |
namespace | string | - | The namespace for the Smithy API. Defaults to your monorepo scope |
computeType | string | ServerlessApiGatewayRestApi | The type of compute to use to deploy this API. |
auth | string | IAM | The method used to authenticate with your API. Choose between IAM (default), Cognito or None. |
directory | string | packages | The directory to store the application in. |
iacProvider | string | Inherit | The preferred IaC provider. By default this is inherited from your initial selection. |
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
app
Directoryapi/
- 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
app
Directoryapi/
Directoryrest-api/
- rest-api.tf Modulo per distribuire un’API REST
- project.json Target di build e configurazione del progetto
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 */ };};
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 run <model-project>:build
yarn nx run <model-project>:build
npx nx run <model-project>:build
bunx nx run <model-project>:build
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 run <backend-project>:copy-ssdk
yarn nx run <backend-project>:copy-ssdk
npx nx run <backend-project>:copy-ssdk
bunx nx run <backend-project>:copy-ssdk
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 run <project-name>:bundle
yarn nx run <project-name>:bundle
npx nx run <project-name>:bundle
bunx nx run <project-name>:bundle
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 run <backend-project>:serve
yarn nx run <backend-project>:serve
npx nx run <backend-project>:serve
bunx nx run <backend-project>:serve
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
:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
# 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}
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"
# 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: [...],}));
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. Esempio per aggiungere il supporto VPC:
# 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 }}
Utilizza il modulo con la configurazione VPC:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
# 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}
Override delle Integrazioni
Sezione intitolata “Override delle Integrazioni”Puoi sovrascrivere le integrazioni per operazioni specifiche usando il metodo withOverrides
. Ogni override deve specificare una proprietà integration
tipizzata correttamente. Esempio per reindirizzare un’API getDocumentation
:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), }, }) .build(),});
Le integrazioni sovrascritte non avranno più la proprietà handler
quando accedute tramite api.integrations.getDocumentation
.
Puoi aggiungere proprietà personalizzate alle integrazioni mantenendo il type-safety. Esempio con integrazione S3:
const storageBucket = new Bucket(this, 'Bucket', { ... });
const apiGatewayRole = new Role(this, 'ApiGatewayS3Role', { assumedBy: new ServicePrincipal('apigateway.amazonaws.com'),});
storageBucket.grantRead(apiGatewayRole);
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getFile: { bucket: storageBucket, integration: new AwsIntegration({ service: 's3', integrationHttpMethod: 'GET', path: `${storageBucket.bucketName}/{fileName}`, options: { credentialsRole: apiGatewayRole, requestParameters: { 'integration.request.path.fileName': 'method.request.querystring.fileName', }, integrationResponses: [{ statusCode: '200' }], }, }), options: { requestParameters: { 'method.request.querystring.fileName': true, }, methodResponses: [{ statusCode: '200', }], } }, }) .build(),});
// Accesso type-safe alla proprietà bucket definitaapi.integrations.getFile.bucket.grantRead(...);
Override degli Authorizer
Sezione intitolata “Override degli Authorizer”Puoi sovrascrivere gli authorizer specificando options
nelle integrazioni. Esempio con autenticazione Cognito:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), options: { authorizer: new CognitoUserPoolsAuthorizer(...) // Per REST API } }, }) .build(),});
Integrazioni Esplicite
Sezione intitolata “Integrazioni Esplicite”Puoi definire manualmente ogni integrazione. Utile per usare tipi diversi per ogni operazione:
new MyApi(this, 'MyApi', { integrations: { sayHello: { integration: new LambdaIntegration(...), }, getDocumentation: { integration: new HttpIntegration(...), }, },});
Per integrazioni esplicite con Terraform, modifica il modulo generato:
- Rimuovi le route proxy predefinite
- Sostituisci la singola Lambda con funzioni individuali
- Crea integrazioni specifiche per ogni operazione:
# Aggiungi funzioni Lambda individuali resource "aws_lambda_function" "say_hello_handler" { handler = "sayHello.handler" # ... configurazione ... }
# Crea integrazioni e route specifiche resource "aws_apigatewayv2_integration" "say_hello_integration" { integration_uri = aws_lambda_function.say_hello_handler.invoke_arn # ... configurazione ... }
# Aggiungi risorse API Gateway specifiche resource "aws_api_gateway_resource" "say_hello_resource" { path_part = "sayHello" }
resource "aws_api_gateway_method" "say_hello_method" { http_method = "POST" }
Router Pattern
Sezione intitolata “Router Pattern”Per usare una singola Lambda per tutte le richieste, modifica defaultIntegrations
:
export class MyApi<...> extends ... {
public static defaultIntegrations = (scope: Construct) => { const router = new Function(scope, 'RouterHandler', { ... }); return IntegrationBuilder.rest({ buildDefaultIntegration: (op) => ({ integration: new LambdaIntegration(router), }), }); };}
Il router pattern è l’approccio predefinito di Terraform:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api" 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 api-connection
, che fornisce generazione type-safe del client dal tuo modello Smithy.