API TypeScript Smithy
Smithy é uma linguagem de definição de interface independente de protocolo para criar APIs de forma orientada a modelos.
O gerador de API TypeScript Smithy cria uma nova API usando Smithy para definição de serviço e o Smithy TypeScript Server SDK para implementação. O gerador fornece infraestrutura como código CDK ou Terraform para implantar seu serviço no AWS Lambda, exposto através de uma API REST do AWS API Gateway. Ele fornece desenvolvimento de API com segurança de tipos e geração automática de código a partir de modelos Smithy. O handler gerado usa AWS Lambda Powertools for TypeScript para observabilidade, incluindo logging, rastreamento AWS X-Ray e CloudWatch Metrics
Gerar uma API TypeScript Smithy
Seção intitulada “Gerar uma API TypeScript Smithy”Você pode gerar uma nova API TypeScript Smithy de duas maneiras:
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=smithyVocê também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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- Instale o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - ts#api - Preencha os parâmetros obrigatórios
- framework: smithy
- Clique em
Generate
| Parâmetro | Tipo | Padrão | Descrição |
|---|---|---|---|
| name Obrigatório | string | - | O nome da API (obrigatório). Usado para gerar nomes de classes e caminhos de arquivos. |
| framework | trpc | smithy | trpc | O framework de API a ser utilizado. |
| namespace | string | - | O namespace para a API Smithy (aplicável apenas para o framework smithy). O padrão é o escopo do seu monorepo |
| integrationPattern | isolated | shared | isolated | Como as integrações do API Gateway são geradas para a API. Escolha entre isolated (padrão) e shared. |
| auth | iam | cognito | custom | iam | O método usado para autenticar com sua API. Escolha entre iam (padrão), cognito ou custom. |
| directory | string | packages | O diretório para armazenar a aplicação. |
| subDirectory | string | - | O subdiretório onde o projeto é colocado. Por padrão, este é o nome do projeto. |
| iac | inherit | cdk | terraform | inherit | O provedor IaC preferido. Por padrão, isso é herdado da sua seleção inicial. |
| infra | rest-lambda | http-lambda | none | rest-lambda | O tipo de infraestrutura a ser usado para implantar esta API. |
| preferInstallDependencies | boolean | true | Se deve preferir instalar dependências após a execução do gerador. Defina como false para adiar a instalação ao executar múltiplos geradores em lote (uma instalação ainda é executada se necessário para que geradores subsequentes possam calcular o grafo de projetos Nx); instale uma vez no final. |
Saída do Gerador
Seção intitulada “Saída do Gerador”O gerador cria dois projetos relacionados no diretório <directory>/<api-name>:
Directorymodel/ Projeto de modelo Smithy
- package.json Manifesto do projeto definindo o nome do pacote e dependências
- project.json Configuração do projeto e alvos de build
- smithy-build.json Configuração de build do Smithy
- ssdk.rolldown.config.mjs Empacota o TypeScript Server SDK gerado
Directorysrc/
- main.smithy Definição principal do serviço
Directoryoperations/
- echo.smithy Definição de operação de exemplo
Directorybackend/ Implementação backend TypeScript
- project.json Configuração do projeto e alvos de build
- rolldown.config.ts Configuração de empacotamento
Directorysrc/
- handler.ts Handler do AWS Lambda
- local-server.ts Servidor de desenvolvimento local
- service.ts Implementação do serviço
- context.ts Definição de contexto do serviço
Directoryoperations/
- echo.ts Implementação de operação de exemplo
Directorygenerated/ SDK TypeScript gerado (criado durante o build)
- …
Infraestrutura
Seção intitulada “Infraestrutura”Como este gerador cria infraestrutura como código baseada no seu iac escolhido, ele criará um projeto em packages/common que inclui os constructs CDK ou módulos Terraform relevantes.
O projeto comum de infraestrutura como código é estruturado da seguinte forma:
Directorypackages/common/constructs
Directorysrc
Directoryapp/ Constructs para infraestrutura específica de um projeto/gerador
Directoryapis/
- <project-name>.ts Construct CDK para implantar sua API
Directorycore/ Constructs genéricos que são reutilizados por constructs em
appDirectoryapi/
- rest-api.ts Construct CDK para implantar uma API REST
- utils.ts Utilitários para os constructs de API
- index.ts Ponto de entrada exportando constructs de
app
- project.json Alvos de build e configuração do projeto
Directorypackages/common/terraform
Directorysrc
Directoryapp/ Módulos Terraform para infraestrutura específica de um projeto/gerador
Directoryapis/
Directory<project-name>/
- <project-name>.tf Módulo para implantar sua API
Directorycore/ Módulos genéricos que são reutilizados por módulos em
appDirectoryapi/
Directoryrest-api/
- rest-api.tf Módulo para implantar uma API REST
- project.json Alvos de build e configuração do projeto
Arquitetura
Seção intitulada “Arquitetura”A API Smithy implantada tem a seguinte arquitetura, com um Web ACL AWS WAFv2 na frente do estágio do API Gateway:
Implementando sua API Smithy
Seção intitulada “Implementando sua API Smithy”Definindo Operações em Smithy
Seção intitulada “Definindo Operações em Smithy”As operações são definidas em arquivos Smithy dentro do projeto de modelo. A definição principal do serviço está em 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 ]}Operações individuais são definidas em arquivos separados no diretório 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}Adicionando uma Biblioteca de Shapes
Seção intitulada “Adicionando uma Biblioteca de Shapes”Se você tiver várias APIs Smithy que compartilham os mesmos tipos de dados, você pode definir esses tipos uma vez em uma biblioteca de shapes em vez de duplicá-los em cada modelo. Uma biblioteca de shapes é um projeto Smithy sem serviço — apenas shapes reutilizáveis — que qualquer número de projetos Smithy pode depender.
Gere uma com o gerador 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=shapesVocê também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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- Instale o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - smithy#project - Preencha os parâmetros obrigatórios
- name: my-shapes
- type: shapes
- Clique em
Generate
O modelo da sua API pode então referenciar seus shapes com use:
$version: "2.0"
namespace com.example.api
use com.example.shared#Customer
structure GetCustomerOutput { @required customer: Customer}Veja o guia de projeto Smithy para saber como criar uma biblioteca de shapes e conectá-la como uma dependência do modelo da sua API.
Implementando Operações em TypeScript
Seção intitulada “Implementando Operações em TypeScript”As implementações de operações estão localizadas no diretório src/operations/ do projeto backend. Cada operação é implementada usando os tipos gerados do TypeScript Server SDK (gerado em tempo de build a partir do seu modelo 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 };};As operações devem ser registradas na definição do serviço em 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};Contexto do Serviço
Seção intitulada “Contexto do Serviço”Você pode definir contexto compartilhado para suas operações em 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;}Este contexto é passado para todas as implementações de operações e pode ser usado para compartilhar recursos como conexões de banco de dados, configuração ou utilitários de logging.
Observabilidade com AWS Lambda Powertools
Seção intitulada “Observabilidade com AWS Lambda Powertools”Logging
Seção intitulada “Logging”O gerador configura logging estruturado usando AWS Lambda Powertools com injeção automática de contexto via middleware Middy.
export const handler = middy<APIGatewayProxyEvent, APIGatewayProxyResult>() .use(captureLambdaHandler(tracer)) .use(injectLambdaContext(logger)) .use(logMetrics(metrics)) .handler(lambdaHandler);Você pode referenciar o logger das suas implementações de operações através do contexto:
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'); // ...};Rastreamento
Seção intitulada “Rastreamento”O rastreamento AWS X-Ray é configurado automaticamente através do middleware captureLambdaHandler.
export const handler = middy<APIGatewayProxyEvent, APIGatewayProxyResult>() .use(captureLambdaHandler(tracer)) .use(injectLambdaContext(logger)) .use(logMetrics(metrics)) .handler(lambdaHandler);Você pode adicionar subsegmentos personalizados aos seus rastreamentos nas suas operações:
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étricas
Seção intitulada “Métricas”As métricas do CloudWatch são coletadas automaticamente para cada requisição através do middleware logMetrics.
export const handler = middy<APIGatewayProxyEvent, APIGatewayProxyResult>() .use(captureLambdaHandler(tracer)) .use(injectLambdaContext(logger)) .use(logMetrics(metrics)) .handler(lambdaHandler);Você pode adicionar métricas personalizadas nas suas operações:
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); // ...};Tratamento de Erros
Seção intitulada “Tratamento de Erros”Smithy fornece tratamento de erros integrado. Você pode definir erros personalizados no seu modelo Smithy:
@error("client")@httpError(400)structure InvalidRequestError { @required message: String}E registrá-los na sua operação/serviço:
operation MyOperation { ... errors: [InvalidRequestError]}Então lançá-los na sua implementação 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 */ };};Acessando o Usuário Chamador
Seção intitulada “Acessando o Usuário Chamador”Quando sua API é protegida por autenticação, suas operações frequentemente precisam saber quem está chamando. A abordagem recomendada é resolver a identidade do chamador uma vez no handler e passá-la através do contexto do serviço para consumo por operações específicas.
Vamos modelar o caso não autorizado como um erro Smithy para que ele serialize para uma resposta 403 adequada. Adicione-o ao seu modelo, por exemplo em model/src/operations/errors.smithy, e referencie-o em qualquer operação que requer identidade:
$version: "2.0"
namespace your.namespace
/// Thrown when the calling user cannot be determined@error("client")@httpError(403)structure UnauthorizedError { @required message: String}Primeiro, exponha a identidade resolvida no contexto do serviço em src/context.ts. Nós a fornecemos como uma função para que o UnauthorizedError seja lançado de dentro de uma operação (onde o Server SDK o serializa para um 403), em vez de do handler:
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>;}Em seguida, escreva o resolvedor em src/identity.ts. Ele lança UnauthorizedError quando o chamador não pode ser determinado. A implementação depende do seu método auth selecionado:
Para autenticação IAM, procuramos o chamador no Cognito usando o sub extraído do evento do 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! };};Com auth: 'cognito', o autorizador Cognito User Pools do API Gateway verifica o JWT que o chamador fornece no cabeçalho Authorization e coloca as claims verificadas no evento em 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 };};Então conecte o resolvedor ao contexto em 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),});Agora podemos usar a identidade resolvida em uma operação, por exemplo em 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 e Geração de Código
Seção intitulada “Build e Geração de Código”O projeto de modelo Smithy usa a Smithy CLI para construir os artefatos Smithy e gerar o TypeScript Server SDK:
pnpm nx build <model-project>yarn nx build <model-project>npx nx build <model-project>bunx nx build <model-project>No macOS e Linux, a CLI é resolvida pelo mise, que o build busca sob demanda, então não há nada para instalar — ele baixa e armazena em cache a versão fixada na primeira vez que você faz o build.
Este processo:
- Compila o modelo Smithy e o valida
- Gera a especificação OpenAPI a partir do modelo Smithy
- Cria o TypeScript Server SDK com interfaces de operação com segurança de tipos
- Gera artefatos de build para
dist/<model-project>/build/
O projeto backend copia automaticamente o SDK gerado durante a compilação:
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 no Windows
Seção intitulada “Build no Windows”mise não publica nenhum pacote Windows para npm, então no Windows a Smithy CLI é um pré-requisito que você instala você mesmo. Instale-a uma vez seguindo o guia de instalação da Smithy CLI (por exemplo winget install smithy ou scoop install smithy), e certifique-se de que smithy está no seu PATH. Um projeto Smithy gerado no Windows executa smithy diretamente em vez de através do mise.
Alternativamente, desenvolva dentro do WSL, onde o build executa o caminho Linux e mise resolve a CLI para você — nada para instalar.
Um projeto gerado no Windows commita um alvo compile que invoca smithy diretamente, então qualquer outra pessoa trabalhando nele — incluindo no macOS ou Linux — precisa da Smithy CLI no seu PATH também. Para que essas máquinas resolvam a CLI através do mise, mude o alvo para o comando mise como descrito abaixo.
Escolhendo como a CLI é resolvida
Seção intitulada “Escolhendo como a CLI é resolvida”macOS e Linux resolvem a CLI através do mise e Windows usa uma CLI instalada globalmente, mas você pode escolher qualquer uma em qualquer plataforma editando o comando do alvo compile no project.json do projeto de modelo.
Para usar uma Smithy CLI instalada globalmente em vez do mise, substitua o prefixo mise por um smithy simples:
{ "targets": { "compile": { "options": { "commands": ["... npx -y mise@<version> exec smithy@<version> -- smithy build ..."] "commands": ["... smithy build ..."] } } }}Para voltar ao mise resolvendo a CLI, restaure o prefixo npx -y mise@<version> exec smithy@<version> --.
Alvo Bundle
Seção intitulada “Alvo Bundle”O gerador configura automaticamente um target bundle que usa Rolldown para criar um pacote de implantação:
pnpm nx bundle <project-name>yarn nx bundle <project-name>npx nx bundle <project-name>bunx nx bundle <project-name>A configuração do Rolldown pode ser encontrada em rolldown.config.ts, com uma entrada por pacote a ser gerado. O Rolldown gerencia a criação de múltiplos pacotes em paralelo, se definidos.
Desenvolvimento Local
Seção intitulada “Desenvolvimento Local”O gerador configura um servidor de desenvolvimento local com hot reloading:
pnpm nx serve <backend-project>yarn nx serve <backend-project>npx nx serve <backend-project>bunx nx serve <backend-project>Implantando sua API Smithy
Seção intitulada “Implantando sua API Smithy”O gerador cria infraestrutura CDK ou Terraform baseada no seu iac selecionado.
O construct CDK para implantar sua API está na pasta 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(), }); }}Isso configura:
- Uma função AWS Lambda para o serviço Smithy
- API Gateway REST API como gatilho da função
- Funções IAM e permissões
- Grupo de logs do CloudWatch
- Configuração de rastreamento X-Ray
Os módulos Terraform para implantar sua API estão na pasta common/terraform.
O módulo da API prepara seu zip de implantação Lambda em um bucket S3 de ativos compartilhado — veja o guia de infraestrutura Terraform para detalhes. Instancie o módulo core/asset-bucket uma vez por implantação e passe sua saída bucket_name para cada módulo de API / Lambda através da entrada 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}Isso configura:
- Uma função AWS Lambda que serve a API Smithy
- API Gateway REST API como gatilho da função
- Funções IAM e permissões
- Grupo de logs do CloudWatch
- Configuração de rastreamento X-Ray
- Configuração CORS
O módulo Terraform fornece várias saídas:
# 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}Para APIs REST, o construto gerado associa um Web ACL do AWS WAFv2 com o estágio do API Gateway por padrão. O Web ACL usa o conjunto de regras padrão gerenciado pela AWS (AWSManagedRulesCommonRuleSet e AWSManagedRulesKnownBadInputsRuleSet), fornecendo proteção contra explorações web comuns, incluindo o OWASP Top 10. Os logs de solicitações do WAF são gravados em um grupo do CloudWatch Logs.
Você pode editar o construto rest-api gerado para adicionar, remover ou ajustar regras (por exemplo, para adicionar regras baseadas em taxa ou grupos de regras gerenciadas adicionais).
Para desativar (por exemplo, para anexar seu próprio Web ACL), defina enableWaf como false:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), enableWaf: false,});Para desativar (por exemplo, para anexar seu próprio Web ACL), defina enable_waf como false:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name enable_waf = false}Logging de acesso
Seção intitulada “Logging de acesso”Para APIs REST, a infraestrutura gerada habilita o registro de acesso por padrão, escrevendo uma linha JSON estruturada por requisição em um grupo dedicado do CloudWatch Logs. O grupo de logs é criptografado com uma chave KMS gerenciada pelo cliente e retido por um ano.
O API Gateway escreve logs de acesso usando uma função do CloudWatch Logs no nível da conta. Esta função é configurada na configuração AWS::ApiGateway::Account, que é um singleton por região por conta — há apenas uma função para cada API REST na região. Para gerenciar isso com segurança em várias pilhas implantadas independentemente, a infraestrutura gerada:
- Cria uma função compartilhada do CloudWatch Logs e a configura na conta apenas quando nenhuma função funcional já está definida, para que as implantações nunca sobrescrevam uma função que outra pilha possui.
- Deixa a configuração da conta intocada na desmontagem, para que destruir uma pilha nunca desabilite o registro para outras APIs REST na região.
A função da conta é gerenciada pelo construto ApiGatewayAccount, um singleton com escopo de pilha resolvido via ApiGatewayAccount.ensure(scope). O estágio de cada API REST depende dele, e a função é configurada por um recurso personalizado baseado em Lambda.
O formato do log de acesso é definido pelo construto RestApi que sua API estende. Para personalizá-lo, passe deployOptions para super no packages/common/constructs/src/app/apis/my-api.ts gerado, mantendo o tracingEnabled que o construto já define:
super(scope, id, { apiName: 'MyApi', // ... deployOptions: { tracingEnabled: true, accessLogFormat: AccessLogFormat.clf(), }, ...props,});AccessLogFormat é importado de aws-cdk-lib/aws-apigateway. Qualquer coisa que você deixar indefinida mantém o padrão do construto — um formato JSON com os campos padrão.
A função da conta é gerenciada pelo módulo core/api/api-gateway-account, que é instanciado pelo módulo de API gerado. Ele configura a conta de forma idempotente e nunca é redefinido no terraform destroy.
Você pode personalizar o formato do log de acesso editando o bloco access_log_settings no recurso aws_api_gateway_stage no módulo de API gerado.
Integrações
Seção intitulada “Integrações”Os construtos CDK de API REST/HTTP são configurados para fornecer uma interface type-safe para definir integrações para cada uma de suas operações.
Os construtos CDK fornecem suporte completo a integrações type-safe conforme descrito abaixo.
Integrações Padrão
Seção intitulada “Integrações Padrão”Você pode usar o método estático defaultIntegrations para fazer uso do padrão default, que define uma função AWS Lambda individual para cada operação:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});Os módulos Terraform usam automaticamente o padrão router com uma única função Lambda. Nenhuma configuração adicional é necessária:
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}Acessando Integrações
Seção intitulada “Acessando Integrações”Você pode acessar as funções AWS Lambda subjacentes através da propriedade integrations do construto da API, de forma type-safe. Por exemplo, se sua API define uma operação chamada sayHello e você precisa adicionar algumas permissões a esta função, você pode fazer isso da seguinte forma:
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: [...],}));Se sua API usa o padrão shared, o Lambda router compartilhado é exposto como api.integrations.$router:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
api.integrations.$router.handler.addEnvironment('LOG_LEVEL', 'DEBUG');Com o padrão router do Terraform, há apenas uma função Lambda. Você pode acessá-la através das saídas do módulo:
# 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/*" } ] })}Personalizando Opções Padrão
Seção intitulada “Personalizando Opções Padrão”Se você deseja personalizar as opções usadas ao criar a função Lambda para cada integração padrão, você pode usar o método withDefaultOptions. Por exemplo, se você deseja que todas as suas funções Lambda residam em uma Vpc:
const vpc = new Vpc(this, 'Vpc', ...);
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withDefaultOptions({ vpc, }) .build(),});A configuração de VPC já é suportada pelo módulo gerado — defina enable_vpc junto com vpc_id e subnet_ids, e o módulo implanta a função Lambda em sua VPC atrás de um grupo de segurança que ele cria para você:
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}Para opções que o módulo não expõe, edite o recurso aws_lambda_function no módulo Terraform gerado diretamente.
Personalizando Opções Por Operação
Seção intitulada “Personalizando Opções Por Operação”Para personalizar as opções usadas para criar a integração padrão para operações específicas (sem afetar as outras), você pode usar o método withOperationOptions. Por exemplo, se você deseja aumentar o timeout da função Lambda para apenas uma operação:
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({ ... }));As opções que você especifica são mescladas com as opções de integração padrão (e quaisquer opções definidas via withDefaultOptions). Observe que você não pode especificar opções para operações que você substituiu via withOverrides, pois estas não usam mais a integração padrão.
Você encontrará um erro de tipo se a mesma operação for alvo de ambos withOperationOptions e withOverrides, independentemente da ordem em que você os chamar.
Para personalizar opções para operações específicas com Terraform, você precisa editar o módulo Terraform gerado para configurar funções Lambda individuais por operação (veja a seção Integrações Explícitas abaixo).
Substituindo Integrações
Seção intitulada “Substituindo Integrações”Você também pode substituir integrações para operações específicas usando o método withOverrides. Cada substituição deve especificar uma propriedade integration que é tipada para o construto de integração CDK apropriado para a API HTTP ou REST. O método withOverrides também é type-safe. Por exemplo, se você deseja substituir uma API getDocumentation para apontar para documentação hospedada por algum site externo, você pode fazer isso da seguinte forma:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), }, }) .build(),});Você também notará que a integração substituída não tem mais uma propriedade handler ao acessá-la via api.integrations.getDocumentation.
Você pode adicionar propriedades adicionais a uma integração que também serão tipadas adequadamente, permitindo que outros tipos de integração sejam abstraídos mas permaneçam type-safe, por exemplo, se você criou uma integração S3 para uma API REST e depois deseja referenciar o bucket para uma operação específica, você pode fazer isso da seguinte forma:
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(...);Substituindo Autorizadores
Seção intitulada “Substituindo Autorizadores”Você também pode fornecer options em sua integração para substituir opções de método específicas, como autorizadores, por exemplo, se você deseja usar autenticação Cognito para sua operação 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(),});Integrações Explícitas
Seção intitulada “Integrações Explícitas”Se você preferir, pode optar por não usar as integrações padrão e, em vez disso, fornecer diretamente uma para cada operação. Isso é útil se, por exemplo, cada operação precisa usar um tipo diferente de integração ou você deseja receber um erro de tipo ao adicionar novas operações:
new MyApi(this, 'MyApi', { integrations: { sayHello: { integration: new LambdaIntegration(...), }, getDocumentation: { integration: new HttpIntegration(...), }, },});Para integrações explícitas por operação com Terraform, você deve modificar o módulo específico do aplicativo gerado para substituir a integração proxy padrão por integrações específicas para cada operação.
Edite packages/common/terraform/src/app/apis/my-api/my-api.tf:
- Remova as rotas proxy padrão (por exemplo,
resource "aws_apigatewayv2_route" "proxy_routes") - Substitua a única função Lambda por funções individuais para cada operação
- Crie integrações e rotas específicas para cada operação, reutilizando o mesmo pacote 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}/*/*" }Padrão de Integração
Seção intitulada “Padrão de Integração”Os construtos de API CDK gerados suportam dois padrões de integração:
isolatedcria uma função Lambda por operação. Este é o padrão para APIs geradas.sharedcria um único Lambda router padrão e o reutiliza para cada operação, a menos que você substitua integrações específicas.
isolated oferece permissões e configuração mais granulares por operação. shared reduz a proliferação de Lambda e integrações do API Gateway, enquanto ainda permite substituições seletivas.
Por exemplo, definir pattern como 'shared' cria uma única função em vez de uma por integração:
export class MyApi<...> extends ... {
public static defaultIntegrations = (scope: Construct) => { ... return IntegrationBuilder.rest({ pattern: 'shared', ... }); };}Os módulos Terraform usam automaticamente o padrão router - esta é a abordagem padrão e única suportada. O módulo gerado cria uma única função Lambda que lida com todas as operações da API.
Você pode simplesmente instanciar o módulo padrão para obter o padrão router:
# 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}Geração de Código
Seção intitulada “Geração de Código”Como as operações são definidas em Smithy, usamos geração de código para fornecer metadados ao construct CDK para integrações com segurança de tipos.
Um alvo generate:<ApiName>-metadata é adicionado ao project.json dos constructs comuns para facilitar esta geração de código, que emite um arquivo como packages/common/constructs/src/generated/my-api/metadata.gen.ts. Como isso é gerado em tempo de build, é ignorado no controle de versão.
Concedendo Acesso (Apenas IAM)
Seção intitulada “Concedendo Acesso (Apenas IAM)”Se você selecionou autenticação IAM, você pode usar o método grantInvokeAccess para conceder acesso à sua 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}Invocando sua API Smithy
Seção intitulada “Invocando sua API Smithy”Para invocar sua API de um website React, você pode usar o gerador connection, que fornece geração de cliente com segurança de tipos a partir do seu modelo Smithy.
Conexões
Seção intitulada “Conexões”Use o gerador connection para integrar este projeto com outros no seu workspace. As seguintes conexões envolvem este projeto: