tRPC
tRPC é um framework para construir APIs em TypeScript com segurança de tipo de ponta a ponta. Usando tRPC, atualizações nas entradas e saídas de operações da API são imediatamente refletidas no código do cliente e são visíveis no seu IDE sem a necessidade de reconstruir seu projeto.
O gerador de API tRPC cria uma nova API tRPC com configuração de infraestrutura AWS CDK ou Terraform. O backend gerado usa AWS Lambda para implantação serverless, exposto via uma API AWS API Gateway, e inclui validação de esquema usando Zod. Ele configura AWS Lambda Powertools para observabilidade, incluindo logging, rastreamento AWS X-Ray e Cloudwatch Metrics.
Gerar uma API tRPC
Seção intitulada “Gerar uma API tRPC”Você pode gerar uma nova API tRPC de duas maneiras:
pnpm nx g @aws/nx-plugin:ts#api --framework=trpcyarn nx g @aws/nx-plugin:ts#api --framework=trpcnpx nx g @aws/nx-plugin:ts#api --framework=trpcbunx nx g @aws/nx-plugin:ts#api --framework=trpcVocê também pode realizar uma execução simulada para ver quais arquivos seriam alterados
pnpm nx g @aws/nx-plugin:ts#api --framework=trpc --dry-runyarn nx g @aws/nx-plugin:ts#api --framework=trpc --dry-runnpx nx g @aws/nx-plugin:ts#api --framework=trpc --dry-runbunx nx g @aws/nx-plugin:ts#api --framework=trpc --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: trpc
- 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 criará a seguinte estrutura de projeto no diretório <directory>/<api-name>:
Directorysrc
- init.ts Backend tRPC initialisation
- handler.ts Lambda handler entrypoint
- router.ts tRPC router definition
Directoryschema Schema definitions using Zod
- echo.ts Example definitions for the input and output of the “echo” procedure
- z-async-iterable.ts Zod helper for subscriptions (REST API only)
Directoryprocedures Procedures (or operations) exposed by your API
- echo.ts Example procedure
Directorymiddleware
- error.ts Middleware for error handling
- logger.ts middleware for configuring AWS Powertools for Lambda logging
- tracer.ts middleware for configuring AWS Powertools for Lambda tracing
- metrics.ts middleware for configuring AWS Powertools for Lambda metrics
- local-server.ts tRPC standalone adapter entrypoint for local development server
Directoryclient
- index.ts Type-safe client for machine-to-machine API calls
- tsconfig.json TypeScript configuration
- package.json Project manifest defining the project’s package name and dependencies
- project.json Project configuration and build targets
Infraestrutura
Seção intitulada “Infraestrutura”Como este gerador fornece infraestrutura como código baseada no seu iac escolhido, ele criará um projeto em packages/common que inclui os constructs CDK relevantes ou módulos Terraform.
O projeto comum de infraestrutura como código é estruturado da seguinte forma:
Directorypackages/common/constructs
Directorysrc
Directoryapp/ Constructs for infrastructure specific to a project/generator
- …
Directorycore/ Generic constructs which are reused by constructs in
app- …
- index.ts Entry point exporting constructs from
app
- project.json Project build targets and configuration
Directorypackages/common/terraform
Directorysrc
Directoryapp/ Terraform modules for infrastructure specific to a project/generator
- …
Directorycore/ Generic modules which are reused by modules in
app- …
- project.json Project build targets and configuration
Para implantar sua API, os seguintes arquivos são gerados:
Directorypackages/common/constructs/src
Directoryapp
Directoryapis
- <project-name>.ts CDK construct for deploying your API
Directorycore
Directoryapi
- http-api.ts CDK construct for deploying an HTTP API (if you selected to deploy an HTTP API)
- rest-api.ts CDK construct for deploying a REST API (if you selected to deploy a REST API)
- utils.ts Utilities for the API constructs
Directorypackages/common/terraform/src
Directoryapp
Directoryapis
Directory<project-name>
- <project-name>.tf Module for deploying your API
Directorycore
Directoryapi
Directoryhttp-api
- http-api.tf Module for deploying an HTTP API (if you selected to deploy an HTTP API)
Directoryrest-api
- rest-api.tf Module for deploying a REST API (if you selected to deploy a REST API)
Arquitetura
Seção intitulada “Arquitetura”A aplicação implantada possui a seguinte arquitetura:
As REST APIs incluem um Web ACL do AWS WAFv2 na frente do estágio do API Gateway com o conjunto de regras padrão gerenciado pela AWS habilitado.
As HTTP APIs não suportam WAF diretamente — se você precisar de proteção WAF, escolha REST API em vez disso ou coloque a HTTP API atrás de uma distribuição CloudFront.
Implementando sua API tRPC
Seção intitulada “Implementando sua API tRPC”Em alto nível, APIs tRPC consistem em um roteador que delega solicitações para procedimentos específicos. Cada procedimento tem uma entrada e saída, definidas como um esquema Zod.
Esquema
Seção intitulada “Esquema”O diretório src/schema contém os tipos que são compartilhados entre seu código cliente e servidor. Neste pacote, esses tipos são definidos usando Zod, uma biblioteca de declaração e validação de esquema TypeScript-first.
Um exemplo de esquema pode parecer o seguinte:
import { z } from 'zod';
// Schema definitionexport const UserSchema = z.object({ name: z.string(), height: z.number(), dateOfBirth: z.string().datetime(),});
// Corresponding TypeScript typeexport type User = z.TypeOf<typeof UserSchema>;Dado o esquema acima, o tipo User é equivalente ao seguinte TypeScript:
interface User { name: string; height: number; dateOfBirth: string;}Esquemas são compartilhados por código servidor e cliente, fornecendo um único lugar para atualizar ao fazer alterações nas estruturas usadas em sua API.
Esquemas são automaticamente validados pela sua API tRPC em tempo de execução, o que economiza a criação manual de lógica de validação personalizada em seu backend.
Zod fornece utilitários poderosos para combinar ou derivar esquemas como .merge, .pick, .omit e mais. Você pode encontrar mais informações no site de documentação do Zod.
Roteador e Procedimentos
Seção intitulada “Roteador e Procedimentos”Seu roteador tRPC é definido em src/router.ts, que registra todos os procedimentos. Cada procedimento define a entrada, saída e implementação esperadas. O ponto de entrada do manipulador Lambda está em src/handler.ts, que encaminha solicitações para seu roteador.
O roteador de exemplo gerado para você tem uma única operação, chamada echo:
import { echo } from './procedures/echo.js';
export const appRouter = router({ echo,});O procedimento de exemplo echo é gerado para você em src/procedures/echo.ts:
export const echo = publicProcedure .input(EchoInputSchema) .output(EchoOutputSchema) .query((opts) => ({ message: opts.input.message }));Para detalhar o acima:
publicProceduredefine um método público na API, incluindo o middleware configurado emsrc/middleware. Este middleware inclui integração AWS Lambda Powertools para logging, rastreamento e métricas.inputaceita um esquema Zod que define a entrada esperada para a operação. Solicitações enviadas para esta operação são automaticamente validadas contra este esquema.outputaceita um esquema Zod que define a saída esperada para a operação. Você verá erros de tipo em sua implementação se não retornar uma saída que esteja em conformidade com o esquema.queryaceita uma função que define a implementação para sua API. Esta implementação recebeopts, que contém ainputpassada para sua operação, bem como outro contexto configurado pelo middleware, disponível emopts.ctx. A função passada paraquerydeve retornar uma saída que esteja em conformidade com o esquemaoutput.
O uso de query para definir a implementação indica que a operação não é mutativa. Use isso para definir métodos para recuperar dados. Para implementar uma operação mutativa, use o método mutation em vez disso.
Se você adicionar um novo procedimento, certifique-se de registrá-lo adicionando-o ao roteador em src/router.ts.
Assinaturas (Streaming)
Seção intitulada “Assinaturas (Streaming)”Assinaturas tRPC permitem que você transmita dados do servidor para o cliente usando Server-Sent Events (SSE). Quando você seleciona rest-lambda como seu tipo de computação, o gerador configura automaticamente a infraestrutura necessária para streaming, bem como um manipulador Lambda de streaming e o auxiliar de esquema ZodAsyncIterable.
Para definir um procedimento de assinatura, use o método .subscription com uma função geradora assíncrona. Use o auxiliar ZodAsyncIterable de src/schema/z-async-iterable.ts para definir o esquema de saída:
import { publicProcedure } from '../init.js';import { z } from 'zod';import { ZodAsyncIterable } from '../schema/z-async-iterable.js';
const InputSchema = z.object({ query: z.string() });const ChunkSchema = z.object({ text: z.string() });
export const myStream = publicProcedure .input(InputSchema) .output( ZodAsyncIterable({ yield: ChunkSchema, }), ) .subscription(async function* (opts) { // Yield data to the client as it becomes available for (const chunk of await getResults(opts.input.query)) { yield { text: chunk }; } });Registre a assinatura em seu roteador assim como qualquer outro procedimento:
export const appRouter = router({ echo, myStream,});A infraestrutura gerada usa um manipulador Lambda de streaming com ResponseTransferMode.STREAM no API Gateway para todas as operações da API REST, o que permite que assinaturas funcionem junto com consultas e mutações regulares.
Personalizando sua API tRPC
Seção intitulada “Personalizando sua API tRPC”Em sua implementação, você pode retornar respostas de erro aos clientes lançando um TRPCError. Estes aceitam um code que indica o tipo de erro, por exemplo:
throw new TRPCError({ code: 'NOT_FOUND', message: 'The requested resource could not be found',});Organizando Suas Operações
Seção intitulada “Organizando Suas Operações”À medida que sua API cresce, você pode desejar agrupar operações relacionadas.
Você pode agrupar operações usando roteadores aninhados, por exemplo:
import { getUser } from './procedures/users/get.js';import { listUsers } from './procedures/users/list.js';
const appRouter = router({ users: router({ get: getUser, list: listUsers, }), ...})Os clientes então recebem este agrupamento de operações, por exemplo, invocar a operação listUsers neste caso pode parecer o seguinte:
client.users.list.query();Logging
Seção intitulada “Logging”O logger AWS Lambda Powertools é configurado em src/middleware/logger.ts, e pode ser acessado em uma implementação de API via opts.ctx.logger. Você pode usar isso para registrar no CloudWatch Logs, e/ou controlar valores adicionais para incluir em cada mensagem de log estruturada. Por exemplo:
export const echo = publicProcedure .input(...) .output(...) .query(async (opts) => { opts.ctx.logger.info('Operation called with input', opts.input);
return ...; });Para mais informações sobre o logger, consulte a documentação do AWS Lambda Powertools Logger.
Registrando Métricas
Seção intitulada “Registrando Métricas”As métricas AWS Lambda Powertools são configuradas em src/middleware/metrics.ts, e podem ser acessadas em uma implementação de API via opts.ctx.metrics. Você pode usar isso para registrar métricas no CloudWatch sem a necessidade de importar e usar o AWS SDK, por exemplo:
export const echo = publicProcedure .input(...) .output(...) .query(async (opts) => { opts.ctx.metrics.addMetric('Invocations', 'Count', 1);
return ...; });Para mais informações, consulte a documentação do AWS Lambda Powertools Metrics.
Ajuste Fino do Rastreamento X-Ray
Seção intitulada “Ajuste Fino do Rastreamento X-Ray”O rastreador AWS Lambda Powertools é configurado em src/middleware/tracer.ts, e pode ser acessado em uma implementação de API via opts.ctx.tracer. Você pode usar isso para adicionar rastreamentos com AWS X-Ray para fornecer insights detalhados sobre o desempenho e fluxo de solicitações de API. Por exemplo:
export const echo = publicProcedure .input(...) .output(...) .query(async (opts) => { const subSegment = opts.ctx.tracer.getSegment()!.addNewSubsegment('MyAlgorithm'); // ... my algorithm logic to capture subSegment.close();
return ...; });Para mais informações, consulte a documentação do AWS Lambda Powertools Tracer.
Implementando Middleware Personalizado
Seção intitulada “Implementando Middleware Personalizado”Você pode adicionar valores adicionais ao contexto fornecido aos procedimentos implementando middleware.
Como exemplo, vamos implementar algum middleware para extrair alguns detalhes sobre o usuário chamador de nossa API em src/middleware/identity.ts.
Este exemplo percorre o middleware de identidade para autenticação IAM. Procuramos o chamador no Cognito usando o sub extraído do evento API Gateway.
Primeiro, definimos o que adicionaremos ao contexto:
export interface IIdentityContext { identity?: { sub: string; username: string; };}Note que definimos uma propriedade adicional opcional para o contexto. tRPC gerencia garantir que isso seja definido em procedimentos que configuraram corretamente este middleware.
Em seguida, implementaremos o middleware em si. Isso tem a seguinte estrutura:
export const createIdentityPlugin = () => { const t = initTRPC.context<...>().create(); return t.procedure.use(async (opts) => { // Add logic here to run before the procedure
const response = await opts.next(...);
// Add logic here to run after the procedure
return response; });};No nosso caso, queremos extrair detalhes sobre o usuário Cognito chamador. Faremos isso extraindo o ID de assunto do usuário (ou “sub”) do evento API Gateway e recuperando detalhes do usuário do Cognito. A implementação varia dependendo se o evento foi fornecido à nossa função por uma API REST ou uma API HTTP:
import { CognitoIdentityProvider } from '@aws-sdk/client-cognito-identity-provider';import { initTRPC, TRPCError } from '@trpc/server';import { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda';import { APIGatewayProxyEvent } from 'aws-lambda';
export interface IIdentityContext { identity?: { sub: string; username: string; };}
export const createIdentityPlugin = () => { const t = initTRPC.context<IIdentityContext & CreateAWSLambdaContextOptions<APIGatewayProxyEvent>>().create();
const cognito = new CognitoIdentityProvider();
return t.procedure.use(async (opts) => { const cognitoAuthenticationProvider = opts.ctx.event.requestContext?.identity?.cognitoAuthenticationProvider;
let sub: string | undefined = undefined; if (cognitoAuthenticationProvider) { const providerParts = cognitoAuthenticationProvider.split(':'); sub = providerParts[providerParts.length - 1]; }
if (!sub) { throw new TRPCError({ code: 'FORBIDDEN', message: `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 TRPCError({ code: 'FORBIDDEN', message: `No user found with subjectId ${sub}`, }); }
// Provide the identity to other procedures in the context return await opts.next({ ctx: { ...opts.ctx, identity: { sub, username: Users[0].Username!, }, }, }); });};import { CognitoIdentityProvider } from '@aws-sdk/client-cognito-identity-provider';import { initTRPC, TRPCError } from '@trpc/server';import { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda';import { APIGatewayProxyEventV2WithIAMAuthorizer } from 'aws-lambda';
export interface IIdentityContext { identity?: { sub: string; username: string; };}
export const createIdentityPlugin = () => { const t = initTRPC.context<IIdentityContext & CreateAWSLambdaContextOptions<APIGatewayProxyEventV2WithIAMAuthorizer>>().create();
const cognito = new CognitoIdentityProvider();
return t.procedure.use(async (opts) => { const cognitoIdentity = opts.ctx.event.requestContext?.authorizer?.iam ?.cognitoIdentity as unknown as | { amr: string[]; } | undefined;
const sub = (cognitoIdentity?.amr ?? []) .flatMap((s) => (s.includes(':CognitoSignIn:') ? [s] : [])) .map((s) => { const parts = s.split(':'); return parts[parts.length - 1]; })?.[0];
if (!sub) { throw new TRPCError({ code: 'FORBIDDEN', message: `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 TRPCError({ code: 'FORBIDDEN', message: `No user found with subjectId ${sub}`, }); }
// Provide the identity to other procedures in the context return await opts.next({ ctx: { ...opts.ctx, identity: { sub, username: Users[0].Username!, }, }, }); });};Quando você implanta com auth: 'cognito', o autorizador Cognito do API Gateway verifica o JWT que o chamador fornece no cabeçalho Authorization e coloca as reivindicações verificadas no evento Lambda. Nosso middleware apenas lê essas reivindicações — sem chamadas extras do AWS SDK, sem verificação manual de JWT.
Primeiro, definimos o que adicionaremos ao contexto:
export interface IIdentityContext { identity?: { sub: string; username: string; };}Note que definimos uma propriedade adicional opcional no contexto. tRPC gerencia garantir que isso seja definido em procedimentos que configuraram corretamente este middleware.
Em seguida, o middleware em si:
import { initTRPC, TRPCError } from '@trpc/server';import { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda';import { APIGatewayProxyEvent } from 'aws-lambda';
export interface IIdentityContext { identity?: { sub: string; username: string; };}
export const createIdentityPlugin = () => { const t = initTRPC .context<IIdentityContext & CreateAWSLambdaContextOptions<APIGatewayProxyEvent>>() .create();
return t.procedure.use(async (opts) => { const claims = opts.ctx.event.requestContext?.authorizer?.claims as | Record<string, string> | undefined;
const sub = claims?.sub; const username = claims?.username;
if (!sub || !username) { throw new TRPCError({ code: 'FORBIDDEN', message: 'Unable to determine calling user', }); }
return await opts.next({ ctx: { ...opts.ctx, identity: { sub, username, }, }, }); });};Você pode então misturar o plugin em qualquer procedimento que precise da identidade do chamador:
import { publicProcedure } from '../init.js';import { createIdentityPlugin } from '../middleware/identity.js';import { z } from 'zod';
export const me = publicProcedure .concat(createIdentityPlugin()) .output(z.object({ sub: z.string(), username: z.string() })) .query(({ ctx }) => ({ sub: ctx.identity!.sub, username: ctx.identity!.username, }));Implantando sua API tRPC
Seção intitulada “Implantando sua API tRPC”O gerador de API tRPC cria infraestrutura como código CDK ou Terraform com base no seu iac selecionado. Você pode usar isso para implantar sua API tRPC.
O construto CDK para implantar sua API está na pasta common/constructs. Você pode consumir isso em uma aplicação CDK, por exemplo:
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(), }); }}import { MyApi, UserIdentity } from '@my-scope/common-constructs';
export class ExampleStack extends Stack { constructor(scope: Construct, id: string) { // Add the api to your stack const identity = new UserIdentity(this, 'Identity');
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), identity, }); }}O construto UserIdentity pode ser gerado usando o gerador ts#website#auth.
Isso configura sua infraestrutura de API, incluindo uma API AWS API Gateway REST ou HTTP, funções AWS Lambda para lógica de negócios e autenticação baseada no seu método auth escolhido.
Os módulos Terraform para implantar sua API estão na pasta common/terraform. Você pode usar isso em uma configuração Terraform.
O módulo de 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 via a 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}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
user_pool_id = local.user_pool_id user_pool_client_ids = [local.client_id]
# 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}Você pode configurar o Cognito User Pool e Client usando os recursos ou módulos Terraform apropriados.
Isso configura:
- Uma função AWS Lambda que serve todos os procedimentos tRPC
- API Gateway HTTP/REST API como gatilho da função
- Funções e permissões IAM
- Grupo de logs CloudWatch
- Configuração de rastreamento X-Ray
- Configuração CORS
O módulo Terraform fornece várias saídas que você pode usar:
# 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}
# Access IAM role for granting additional permissionsoutput "lambda_execution_role_arn" { value = module.my_api.lambda_execution_role_arn}Você pode personalizar as configurações CORS passando variáveis para o módulo:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# Custom CORS configuration cors_allow_origins = ["https://myapp.com", "https://staging.myapp.com"] cors_allow_methods = ["GET", "POST", "PUT", "DELETE"] cors_allow_headers = [ "authorization", "content-type", "x-custom-header" ]
tags = local.common_tags}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}Registro de acesso
Seção intitulada “Registro 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}Concedendo Acesso (Somente IAM)
Seção intitulada “Concedendo Acesso (Somente IAM)”Você pode conceder acesso à sua API da seguinte forma:
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 tRPC 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 role (e.g., for authenticated users)resource "aws_iam_role_policy_attachment" "api_invoke_access" { role = aws_iam_role.authenticated_user_role.name policy_arn = aws_iam_policy.api_invoke_policy.arn}
# Or attach to an existing role by nameresource "aws_iam_role_policy_attachment" "api_invoke_access_existing" { role = "MyExistingRole" policy_arn = aws_iam_policy.api_invoke_policy.arn}As principais saídas do módulo de API que você pode usar para políticas IAM são:
module.my_api.api_execution_arn- Para conceder permissões execute-api:Invokemodule.my_api.api_arn- O ARN do API Gatewaymodule.my_api.lambda_function_arn- O ARN da função Lambda
Bundle Target
Seção intitulada “Bundle Target”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.
Servidor tRPC Local
Seção intitulada “Servidor tRPC Local”Você pode usar o target serve para executar um servidor local para sua API, por exemplo:
pnpm nx serve my-apiyarn nx serve my-apinpx nx serve my-apibunx nx serve my-apiO ponto de entrada para o servidor local é src/local-server.ts.
Isso recarregará automaticamente quando você fizer alterações em sua API.
Invocando sua API tRPC
Seção intitulada “Invocando sua API tRPC”Você pode criar um cliente tRPC para invocar sua API de maneira type-safe. Se você estiver chamando sua API tRPC de outro backend, você pode usar o cliente em src/client/index.ts, por exemplo:
import { createMyApiClient } from '@my-scope/my-api';
const client = createMyApiClient({ url: 'https://my-api-url.example.com/' });
await client.echo.query({ message: 'Hello world!' });Se você estiver chamando sua API de um site React, considere usar o gerador Connection para configurar o cliente.
Mais Informações
Seção intitulada “Mais Informações”Para mais informações sobre tRPC, consulte a documentação do tRPC.
Conexões
Seção intitulada “Conexões”Use o gerador connection para integrar este projeto com outros em seu workspace. As seguintes conexões envolvem este projeto: