tRPC
tRPC es un framework para construir APIs en TypeScript con seguridad de tipos de extremo a extremo. Usando tRPC, las actualizaciones a las entradas y salidas de las operaciones de la API se reflejan inmediatamente en el código del cliente y son visibles en tu IDE sin necesidad de reconstruir tu proyecto.
El generador de API tRPC crea una nueva API tRPC con configuración de infraestructura AWS CDK o Terraform. El backend generado utiliza AWS Lambda para despliegue serverless, expuesto a través de una API de AWS API Gateway, e incluye validación de esquemas usando Zod. Configura AWS Lambda Powertools para observabilidad, incluyendo logging, trazado de AWS X-Ray y métricas de Cloudwatch.
Generar una API tRPC
Sección titulada «Generar una API tRPC»Puedes generar una nueva API tRPC de dos maneras:
Ejecute este generador@aws/nx-plugin:ts#api
pnpm nx g @aws/nx-plugin:ts#api yarn nx g @aws/nx-plugin:ts#api npx nx g @aws/nx-plugin:ts#api bunx nx g @aws/nx-plugin:ts#api- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - ts#api - Complete los parámetros requeridos
- Haga clic en
Generate
Construya su comando9
Requerido
Opciones
Sección titulada «Opciones»nameRequeridostringEl nombre de la API (requerido). Se utiliza para generar nombres de clases y rutas de archivos.
frameworkenumPredeterminado:trpcEl framework de API a utilizar.
trpcsmithyintegrationPatternenumPredeterminado:isolatedCómo se generan las integraciones de API Gateway para la API. Elija entre isolated (predeterminado) y shared.
isolatedsharedauthenumPredeterminado:iamEl método utilizado para autenticar con tu API. Elige entre iam (predeterminado), cognito o custom.
iamcognitocustomdirectorystringPredeterminado:packagesEl directorio donde almacenar la aplicación.
iacenumPredeterminado:inheritEl proveedor de IaC preferido. Por defecto, se hereda de tu selección inicial.
inheritcdkterraforminfraenumPredeterminado:rest-lambdaEl tipo de infraestructura a utilizar para desplegar esta API.
rest-lambdahttp-lambdanonesubDirectorystringEl subdirectorio en el que se coloca el proyecto. Por defecto es el nombre del proyecto.
preferInstallDependenciesbooleanPredeterminado:trueSi se prefiere instalar las dependencias después de que se ejecute el generador. Establece en false para diferir la instalación cuando se ejecutan múltiples generadores en lote (la instalación aún se ejecuta si es necesario para que los generadores subsecuentes puedan calcular el grafo de proyectos de Nx); instala una vez al final.
Salida del Generador
Sección titulada «Salida del Generador»El generador creará la siguiente estructura de proyecto en el directorio <directory>/<api-name>:
Directoriosrc
- index.ts Package entrypoint re-exporting the router, context, client and schema
- init.ts Backend tRPC initialisation
- handler.ts Lambda handler entrypoint
- router.ts tRPC router definition
Directorioschema Schema definitions using Zod
- index.ts Barrel re-exporting every schema
- echo.ts Example definitions for the input and output of the “echo” procedure
- z-async-iterable.ts Zod helper for subscriptions (REST API only)
Directorioprocedures Procedures (or operations) exposed by your API
- echo.ts Example procedure
Directoriomiddleware
- index.ts Barrel re-exporting the middleware, and the procedure context type
- 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
Directorioclient
- index.ts Type-safe client for machine-to-machine API calls
- rolldown.config.ts Bundle configuration for the Lambda deployment package
- tsconfig.json TypeScript configuration
- tsconfig.lib.json TypeScript configuration for the library sources
- tsconfig.spec.json TypeScript configuration for the tests
- vitest.config.mts Vitest configuration
- package.json Project manifest defining the project’s package name and dependencies
- project.json Project configuration and build targets
- README.md Project readme
- .gitignore Ignores the project’s build output
Infraestructura
Sección titulada «Infraestructura»Dado que este generador proporciona infraestructura como código basada en tu iac elegido, creará un proyecto en packages/common que incluye las construcciones CDK o módulos Terraform relevantes.
El proyecto común de infraestructura como código está estructurado de la siguiente manera:
Directoriopackages/common/constructs
Directoriosrc
Directorioapp/ Constructs for infrastructure specific to a project/generator
- …
Directoriocore/ Generic constructs which are reused by constructs in
app- …
- index.ts Entry point exporting constructs from
app
- project.json Project build targets and configuration
Directoriopackages/common/terraform
Directoriosrc
Directorioapp/ Terraform modules for infrastructure specific to a project/generator
- …
Directoriocore/ Generic modules which are reused by modules in
app- …
- project.json Project build targets and configuration
Para desplegar tu API, se generan los siguientes archivos:
Directoriopackages/common/constructs/src
Directorioapp
Directorioapis
- <project-name>.ts CDK construct for deploying your API
Directoriocore
Directorioapi
- 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
Directoriopackages/common/terraform/src
Directorioapp
Directorioapis
Directorio<project-name>
- <project-name>.tf Module for deploying your API
Directoriocore
Directorioapi
Directoriohttp-api
- http-api.tf Module for deploying an HTTP API (if you selected to deploy an HTTP API)
Directoriorest-api
- rest-api.tf Module for deploying a REST API (if you selected to deploy a REST API)
Arquitectura
Sección titulada «Arquitectura»La aplicación desplegada tiene la siguiente arquitectura: una API de API Gateway frente a una función Lambda que ejecuta tu manejador.
Las REST APIs incluyen una Web ACL de AWS WAFv2 frente a la etapa de API Gateway con el conjunto de reglas predeterminado administrado por AWS habilitado.
Las HTTP APIs no admiten WAF directamente — si necesitas protección WAF, elige REST API en su lugar o coloca la HTTP API detrás de una distribución de CloudFront.
Implementando tu API tRPC
Sección titulada «Implementando tu API tRPC»A alto nivel, las APIs tRPC consisten en un router que delega solicitudes a procedimientos específicos. Cada procedimiento tiene una entrada y una salida, definidas como un esquema Zod.
Esquema
Sección titulada «Esquema»El directorio src/schema contiene los tipos que se comparten entre tu código de cliente y servidor. En este paquete, estos tipos se definen usando Zod, una biblioteca de declaración y validación de esquemas con TypeScript como prioridad.
Un esquema de ejemplo podría verse de la siguiente manera:
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 el esquema anterior, el tipo User es equivalente al siguiente TypeScript:
interface User { name: string; height: number; dateOfBirth: string;}Los esquemas son compartidos tanto por el código del servidor como del cliente, proporcionando un único lugar para actualizar al hacer cambios en las estructuras utilizadas en tu API.
Los esquemas son validados automáticamente por tu API tRPC en tiempo de ejecución, lo que ahorra tener que crear lógica de validación personalizada en tu backend.
Zod proporciona utilidades poderosas para combinar o derivar esquemas como .merge, .pick, .omit y más. Puedes encontrar más información en el sitio web de documentación de Zod.
Router y Procedimientos
Sección titulada «Router y Procedimientos»Tu router tRPC está definido en src/router.ts, que registra todos los procedimientos. Cada procedimiento define la entrada, salida e implementación esperadas. El punto de entrada del manejador Lambda está en src/handler.ts, que reenvía las solicitudes a tu router.
El router de ejemplo generado para ti tiene una sola operación, llamada echo:
import { echo } from './procedures/echo.js';
export const appRouter = router({ echo,});El procedimiento de ejemplo echo se genera para ti en src/procedures/echo.ts:
export const echo = publicProcedure .input(EchoInputSchema) .output(EchoOutputSchema) .query((opts) => ({ message: opts.input.message }));Para desglosar lo anterior:
publicProceduredefine un método público en la API, incluyendo el middleware configurado ensrc/middleware. Este middleware incluye integración de AWS Lambda Powertools para logging, trazado y métricas.inputacepta un esquema Zod que define la entrada esperada para la operación. Las solicitudes enviadas para esta operación se validan automáticamente contra este esquema.outputacepta un esquema Zod que define la salida esperada para la operación. Verás errores de tipo en tu implementación si no devuelves una salida que se ajuste al esquema.queryacepta una función que define la implementación para tu API. Esta implementación recibeopts, que contiene elinputpasado a tu operación, así como otro contexto configurado por el middleware, disponible enopts.ctx. La función pasada aquerydebe devolver una salida que se ajuste al esquemaoutput.
El uso de query para definir la implementación indica que la operación no es mutativa. Usa esto para definir métodos para recuperar datos. Para implementar una operación mutativa, usa el método mutation en su lugar.
Si agregas un nuevo procedimiento, asegúrate de registrarlo agregándolo al router en src/router.ts.
Suscripciones (Streaming)
Sección titulada «Suscripciones (Streaming)»Las suscripciones tRPC te permiten transmitir datos del servidor al cliente usando Server-Sent Events (SSE). Cuando seleccionas rest-lambda como tu tipo de cómputo, el generador configura automáticamente la infraestructura requerida para streaming, así como un manejador Lambda de streaming y el helper de esquema ZodAsyncIterable.
Para definir un procedimiento de suscripción, usa el método .subscription con una función generadora asíncrona. Usa el helper ZodAsyncIterable de src/schema/z-async-iterable.ts para definir el esquema de salida:
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 }; } });Registra la suscripción en tu router como cualquier otro procedimiento:
export const appRouter = router({ echo, myStream,});La infraestructura generada usa un manejador Lambda de streaming con ResponseTransferMode.STREAM en API Gateway para todas las operaciones de la API REST, lo que permite que las suscripciones funcionen junto con consultas y mutaciones regulares.
Personalizando tu API tRPC
Sección titulada «Personalizando tu API tRPC»Errores
Sección titulada «Errores»En tu implementación, puedes devolver respuestas de error a los clientes lanzando un TRPCError. Estos aceptan un code que indica el tipo de error, por ejemplo:
throw new TRPCError({ code: 'NOT_FOUND', message: 'The requested resource could not be found',});Organizando tus Operaciones
Sección titulada «Organizando tus Operaciones»A medida que tu API crece, es posible que desees agrupar operaciones relacionadas.
Puedes agrupar operaciones usando routers anidados, por ejemplo:
import { getUser } from './procedures/users/get.js';import { listUsers } from './procedures/users/list.js';
const appRouter = router({ users: router({ get: getUser, list: listUsers, }), ...})Los clientes reciben esta agrupación de operaciones, por ejemplo, invocar la operación listUsers en este caso podría verse de la siguiente manera:
client.users.list.query();Logging
Sección titulada «Logging»El logger de AWS Lambda Powertools está configurado en src/middleware/logger.ts, y se puede acceder a él en una implementación de API a través de opts.ctx.logger. Puedes usar esto para registrar en CloudWatch Logs, y/o controlar valores adicionales para incluir en cada mensaje de log estructurado. Por ejemplo:
export const echo = publicProcedure .input(...) .output(...) .query(async (opts) => { opts.ctx.logger.info('Operation called with input', opts.input);
return ...; });Para más información sobre el logger, consulta la documentación de AWS Lambda Powertools Logger.
Registrando Métricas
Sección titulada «Registrando Métricas»Las métricas de AWS Lambda Powertools están configuradas en src/middleware/metrics.ts, y se puede acceder a ellas en una implementación de API a través de opts.ctx.metrics. Puedes usar esto para registrar métricas en CloudWatch sin necesidad de importar y usar el AWS SDK, por ejemplo:
export const echo = publicProcedure .input(...) .output(...) .query(async (opts) => { opts.ctx.metrics.addMetric('Invocations', 'Count', 1);
return ...; });Para más información, consulta la documentación de AWS Lambda Powertools Metrics.
Ajustando el Trazado X-Ray
Sección titulada «Ajustando el Trazado X-Ray»El tracer de AWS Lambda Powertools está configurado en src/middleware/tracer.ts, y se puede acceder a él en una implementación de API a través de opts.ctx.tracer. Puedes usar esto para agregar trazas con AWS X-Ray para proporcionar información detallada sobre el rendimiento y el flujo de las solicitudes de API. Por ejemplo:
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 más información, consulta la documentación de AWS Lambda Powertools Tracer.
Implementando Middleware Personalizado
Sección titulada «Implementando Middleware Personalizado»Puedes agregar valores adicionales al contexto proporcionado a los procedimientos implementando middleware.
Como ejemplo, implementemos un middleware para extraer algunos detalles sobre el usuario que llama a nuestra API en src/middleware/identity.ts.
Este ejemplo recorre el middleware de identidad para autenticación IAM. Buscamos al llamador en Cognito usando el sub extraído del evento de API Gateway.
La búsqueda utiliza el cliente Cognito Identity Provider, que no es una dependencia de una API tRPC generada. Instálalo primero en tu proyecto de API:
pnpm add @aws-sdk/client-cognito-identity-provider@3.1126.0 --filter my-apiyarn workspace @my-scope/my-api add @aws-sdk/client-cognito-identity-provider@3.1126.0npm install --legacy-peer-deps @aws-sdk/client-cognito-identity-provider@3.1126.0 -w packages/my-apibun add @aws-sdk/client-cognito-identity-provider@3.1126.0 --cwd packages/my-apiPrimero, definimos lo que agregaremos al contexto:
export interface IIdentityContext { identity?: { sub: string; username: string; };}Ten en cuenta que definimos una propiedad adicional opcional en el contexto. tRPC se encarga de asegurar que esto esté definido en los procedimientos que han configurado correctamente este middleware.
A continuación, implementaremos el middleware en sí. Esto tiene la siguiente estructura:
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; });};En nuestro caso, queremos extraer detalles sobre el usuario de Cognito que llama. Lo haremos extrayendo el ID de sujeto del usuario (o “sub”) del evento de API Gateway, y recuperando los detalles del usuario de Cognito. La implementación varía dependiendo de si el evento fue proporcionado a nuestra función por una API REST o una API HTTP:
import { CognitoIdentityProvider } from '@aws-sdk/client-cognito-identity-provider';import { initTRPC, TRPCError } from '@trpc/server';import type { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda';import type { 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 type { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda';import type { 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!, }, }, }); });};Cuando despliegas con auth: 'cognito', el autorizador de Cognito de API Gateway verifica el JWT que el llamador proporciona en el encabezado Authorization y coloca las reclamaciones verificadas en el evento Lambda. Nuestro middleware simplemente lee esas reclamaciones: sin llamadas adicionales al AWS SDK, sin verificación manual de JWT.
Primero, definimos lo que agregaremos al contexto:
export interface IIdentityContext { identity?: { sub: string; username: string; };}Ten en cuenta que definimos una propiedad adicional opcional en el contexto. tRPC se encarga de asegurar que esto esté definido en los procedimientos que han configurado correctamente este middleware.
A continuación, el middleware en sí. El tipo de evento y la ubicación de las reclamaciones difieren entre una API REST y una API HTTP, por lo que la implementación depende de tu infra seleccionado:
El autorizador de Cognito User Pools de una API REST coloca las reclamaciones en event.requestContext.authorizer.claims:
import { initTRPC, TRPCError } from '@trpc/server';import type { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda';import type { 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 ?? claims?.['cognito: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, }, }, }); });};El autorizador JWT de una API HTTP entrega un evento payload-v2 cuyas reclamaciones se encuentran un nivel más profundo, en event.requestContext.authorizer.jwt.claims. El contexto debe estar tipado en APIGatewayProxyEventV2WithJWTAuthorizer para coincidir con el que usa el publicProcedure generado; de lo contrario, .concat() falla con el error Context mismatch de tRPC:
import { initTRPC, TRPCError } from '@trpc/server';import type { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda';import type { APIGatewayProxyEventV2WithJWTAuthorizer } from 'aws-lambda';
export interface IIdentityContext { identity?: { sub: string; username: string; };}
export const createIdentityPlugin = () => { const t = initTRPC .context< IIdentityContext & CreateAWSLambdaContextOptions<APIGatewayProxyEventV2WithJWTAuthorizer> >() .create();
return t.procedure.use(async (opts) => { const claims = opts.ctx.event.requestContext?.authorizer?.jwt?.claims as | Record<string, string> | undefined;
const sub = claims?.sub; const username = claims?.username ?? claims?.['cognito: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, }, }, }); });};Luego puedes mezclar el plugin en cualquier procedimiento que necesite la identidad del llamador:
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, }));Desplegando tu API tRPC
Sección titulada «Desplegando tu API tRPC»El generador de API tRPC crea infraestructura como código CDK o Terraform basada en tu iac seleccionado. Puedes usar esto para desplegar tu API tRPC.
El constructo CDK para desplegar tu API se encuentra en la carpeta common/constructs. Puedes consumir esto en una aplicación CDK, por ejemplo:
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, }); }}El constructo UserIdentity puede ser generado usando el generador ts#website#auth.
Esto configura tu infraestructura de API, incluyendo una API REST o HTTP de AWS API Gateway, funciones AWS Lambda para lógica de negocio, y autenticación basada en tu método auth elegido.
Los módulos Terraform para desplegar tu API están en la carpeta common/terraform. Puedes usar esto en una configuración Terraform.
El módulo de API almacena su zip de despliegue Lambda en un bucket S3 de activos compartido: consulta la guía de infraestructura Terraform para más detalles. Instancia el módulo core/asset-bucket una vez por despliegue y pasa su salida bucket_name a cada módulo de API / Lambda a través de la 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}Puedes configurar el Cognito User Pool y Client usando los recursos o módulos Terraform apropiados.
Esto configura:
- Una función AWS Lambda que sirve todos los procedimientos tRPC
- API Gateway HTTP/REST API como el disparador de la función
- Roles IAM y permisos
- Grupo de logs de CloudWatch
- Configuración de trazado X-Ray
- Configuración CORS
El módulo Terraform proporciona varias salidas que puedes 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}Puedes personalizar la configuración CORS pasando variables al 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 las API REST, el constructo generado asocia una Web ACL de AWS WAFv2 con la etapa de API Gateway de forma predeterminada. La Web ACL utiliza el conjunto de reglas predeterminado administrado por AWS (AWSManagedRulesCommonRuleSet y AWSManagedRulesKnownBadInputsRuleSet), proporcionando protección contra exploits web comunes, incluidos los OWASP Top 10. Los registros de solicitudes de WAF se escriben en un grupo de CloudWatch Logs.
Puedes editar el constructo rest-api generado para agregar, eliminar o ajustar reglas (por ejemplo, para agregar reglas basadas en tasa o grupos de reglas administradas adicionales).
Para optar por no participar (por ejemplo, para adjuntar tu propia Web ACL), establece enableWaf en false:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), enableWaf: false,});Para optar por no participar (por ejemplo, para adjuntar tu propia Web ACL), establece enable_waf en 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 acceso
Sección titulada «Registro de acceso»Para las REST APIs, la infraestructura generada habilita el registro de acceso de forma predeterminada, escribiendo una línea JSON estructurada por solicitud en un grupo de CloudWatch Logs dedicado. El grupo de logs está cifrado con una clave KMS administrada por el cliente y se retiene durante un año.
API Gateway escribe los logs de acceso utilizando un rol de CloudWatch Logs a nivel de cuenta. Este rol se configura en la configuración AWS::ApiGateway::Account, que es un singleton por región por cuenta: solo hay un rol para cada REST API en la región. Para gestionar esto de forma segura en múltiples stacks implementados de forma independiente, la infraestructura generada:
- Crea un rol compartido de CloudWatch Logs y lo configura en la cuenta solo cuando no hay un rol funcional ya establecido, por lo que las implementaciones nunca sobrescriben un rol que pertenece a otro stack.
- Deja la configuración de la cuenta intacta durante el desmontaje, por lo que destruir un stack nunca deshabilita el registro para otras REST APIs en la región.
El rol de cuenta es gestionado por el constructo ApiGatewayAccount, un singleton con ámbito de stack resuelto mediante ApiGatewayAccount.ensure(scope). La etapa de cada REST API depende de él, y el rol se configura mediante un recurso personalizado respaldado por Lambda.
El formato del log de acceso es establecido por el constructo RestApi que tu API extiende. Para personalizarlo, pasa deployOptions a super en el archivo generado packages/common/constructs/src/app/apis/my-api.ts, manteniendo el tracingEnabled que el constructo ya establece:
super(scope, id, { apiName: 'MyApi', // ... deployOptions: { tracingEnabled: true, accessLogFormat: AccessLogFormat.clf(), }, ...props,});AccessLogFormat se importa desde aws-cdk-lib/aws-apigateway. Cualquier cosa que dejes sin configurar mantiene el valor predeterminado del constructo: un formato JSON con los campos estándar.
El rol de cuenta es gestionado por el módulo core/api/api-gateway-account, que es instanciado por el módulo de API generado. Configura la cuenta de forma idempotente y nunca se restablece con terraform destroy.
Puedes personalizar el formato del log de acceso editando el bloque access_log_settings en el recurso aws_api_gateway_stage en el módulo de API generado.
Integraciones
Sección titulada «Integraciones»Los constructos CDK de API REST/HTTP están configurados para proporcionar una interfaz con seguridad de tipos para definir integraciones para cada una de sus operaciones.
Integraciones Predeterminadas
Sección titulada «Integraciones Predeterminadas»Puede usar el método estático defaultIntegrations para hacer uso del patrón predeterminado, que define una función AWS Lambda individual para cada operación:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});El módulo generado ya define las integraciones predeterminadas para el patrón con el que se generó la API, por lo que no se necesita configuración adicional:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
tags = local.common_tags}Con el patrón isolated predeterminado, esto crea una función Lambda por operación.
Acceso a Integraciones
Sección titulada «Acceso a Integraciones»Puede acceder a las funciones AWS Lambda subyacentes a través de la propiedad integrations del constructo de API, de manera segura en cuanto a tipos. Por ejemplo, si su API define una operación llamada sayHello y necesita agregar algunos permisos a esta función, puede hacerlo de la siguiente manera:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
// sayHello is typed to the operations defined in your APIapi.integrations.sayHello.handler.addToRolePolicy(new PolicyStatement({ effect: Effect.ALLOW, actions: [...], resources: [...],}));Si su API utiliza el patrón shared, el enrutador Lambda compartido se expone como api.integrations.$router:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
api.integrations.$router.handler.addEnvironment('LOG_LEVEL', 'DEBUG');Tenga en cuenta que $router ya no está disponible si anula todas las operaciones a través de withOverrides, ya que ninguna operación queda usando la integración de enrutador predeterminada.
Con el patrón isolated, las salidas del módulo son mapas indexados por nombre de operación, por lo que puede acceder a los recursos de una sola operación. Por ejemplo, para otorgar permisos adicionales a la función Lambda de una operación:
# Grant additional permissions to just the sayHello operation's functionresource "aws_iam_role_policy" "say_hello_permissions" { name = "say-hello-additional-permissions" role = module.my_api.lambda_execution_role_names["sayHello"]
policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "s3:GetObject", "s3:PutObject" ] Resource = "arn:aws:s3:::my-bucket/*" } ] })}Para otorgar los mismos permisos a todas las operaciones, itere la salida operations:
resource "aws_iam_role_policy" "additional_permissions" { for_each = toset(module.my_api.operations)
name = "additional-api-permissions" role = module.my_api.lambda_execution_role_names[each.key]
policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = ["s3:GetObject"] Resource = "arn:aws:s3:::my-bucket/*" } ] })}El módulo también expone lambda_function_names, lambda_function_arns, lambda_invoke_arns, integration_ids y lambda_log_group_names como mapas indexados por nombre de operación. Con el patrón shared, se exponen en su lugar las salidas singulares equivalentes (lambda_execution_role_name, lambda_function_name, …), ya que solo hay una función.
Los permisos que necesita cada operación es mejor pasarlos al módulo, que los aplica al rol de cada función:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
additional_iam_policy_statements = [ { Effect = "Allow" Action = ["s3:GetObject"] Resource = ["arn:aws:s3:::my-bucket/*"] } ]}Personalización de Opciones Predeterminadas
Sección titulada «Personalización de Opciones Predeterminadas»Si desea personalizar las opciones utilizadas al crear la función Lambda para cada integración predeterminada, puede usar el método withDefaultOptions. Por ejemplo, si desea que todas sus funciones Lambda residan en una Vpc:
const vpc = new Vpc(this, 'Vpc', ...);
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withDefaultOptions({ vpc, }) .build(),});La configuración de VPC ya es compatible con el módulo generado — establezca enable_vpc junto con vpc_id y subnet_ids, y el módulo desplegará cada función Lambda en su VPC detrás de un grupo de seguridad compartido que crea para usted:
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 opciones que el módulo no expone, edite el recurso aws_lambda_function en el módulo Terraform generado directamente. Con el patrón isolated, ese único recurso se declara for_each = local.operations, por lo que una edición allí se aplica a todas las operaciones.
Personalización de Opciones por Operación
Sección titulada «Personalización de Opciones por Operación»Para personalizar las opciones utilizadas para crear la integración predeterminada para operaciones específicas (sin afectar a las demás), puede usar el método withOperationOptions. Por ejemplo, si desea aumentar el tiempo de espera de la función Lambda para solo una operación:
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({ ... }));Las opciones que especifique se fusionan con las opciones de integración predeterminadas (y cualquier opción establecida a través de withDefaultOptions). Tenga en cuenta que no puede especificar opciones para operaciones que haya reemplazado a través de withOverrides, ya que estas ya no usan la integración predeterminada.
Encontrará un error de tipo si la misma operación es objetivo tanto de withOperationOptions como de withOverrides, independientemente del orden en que los llame.
Con el patrón isolated, el recurso de función Lambda ya es por operación, por lo que las opciones pueden variar según el nombre de la operación. Por ejemplo, para dar a una operación un tiempo de espera más largo, edite el recurso aws_lambda_function en el módulo generado:
resource "aws_lambda_function" "api_lambda" { for_each = local.operations
# Default to 30 seconds, but allow longer for specific operations timeout = lookup({ sayHello = 60 }, each.key, 30)
# ... rest of configuration}Anulación de Integraciones
Sección titulada «Anulación de Integraciones»También puede anular integraciones para operaciones específicas usando el método withOverrides. Cada anulación debe especificar una propiedad integration que esté tipada al constructo de integración CDK apropiado para la API HTTP o REST. El método withOverrides también es seguro en cuanto a tipos. Por ejemplo, si desea anular una API getDocumentation para que apunte a documentación alojada por algún sitio web externo, podría lograrlo de la siguiente manera:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), }, }) .build(),});También notará que la integración anulada ya no tiene una propiedad handler al acceder a ella a través de api.integrations.getDocumentation.
Puede agregar propiedades adicionales a una integración que también se tipará en consecuencia, permitiendo que otros tipos de integración se abstraigan pero permanezcan seguros en cuanto a tipos, por ejemplo, si ha creado una integración S3 para una API REST y luego desea hacer referencia al bucket para una operación en particular, puede hacerlo de la siguiente manera:
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(...);Para apuntar una operación específica a un tipo de integración diferente, exclúyala del for_each predeterminado y declare su integración por separado. Por ejemplo, para servir getDocumentation desde un sitio web externo:
# Exclude the overridden operation from the default per-operation resourceslocals { overridden_operations = ["getDocumentation"] default_operations = { for op, details in local.operations : op => details if !contains(local.overridden_operations, op) }}
# Then use local.default_operations in place of local.operations for the# aws_lambda_function, aws_iam_role, aws_apigatewayv2_integration and# aws_lambda_permission resources, and add the override:resource "aws_apigatewayv2_integration" "get_documentation" { api_id = module.http_api.api_id integration_type = "HTTP_PROXY" integration_uri = "https://example.com/documentation" integration_method = "GET"}
resource "aws_apigatewayv2_route" "get_documentation" { api_id = module.http_api.api_id route_key = local.route_key["getDocumentation"] target = "integrations/${aws_apigatewayv2_integration.get_documentation.id}"}Anulación de Autorizadores
Sección titulada «Anulación de Autorizadores»También puede proporcionar options en su integración para anular opciones de método particulares como autorizadores, por ejemplo, si desea usar autenticación Cognito para su operación 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(),});La autorización se establece en la ruta (HTTP API) o método (REST API) para cada operación, por lo que puede variar según el nombre de la operación. Por ejemplo, para dejar una operación sin autenticación en una HTTP API:
resource "aws_apigatewayv2_route" "operation_routes" { for_each = local.operations
# ... rest of configuration
authorization_type = each.key == "getDocumentation" ? "NONE" : "AWS_IAM"}Para una REST API autenticada con IAM, también agregue una declaración de política de recursos que permita el acceso no autenticado a la ruta de esa operación.
Integraciones Explícitas
Sección titulada «Integraciones Explícitas»Si lo prefiere, puede optar por no usar las integraciones predeterminadas y en su lugar proporcionar directamente una para cada operación. Esto es útil si, por ejemplo, cada operación necesita usar un tipo diferente de integración o si desea recibir un error de tipo al agregar nuevas operaciones:
new MyApi(this, 'MyApi', { integrations: { sayHello: { integration: new LambdaIntegration(...), }, getDocumentation: { integration: new HttpIntegration(...), }, },});Reemplace el for_each utilizado por el patrón isolated con instanciaciones explícitas de las funciones Lambda, integraciones y permisos para cada operación.
Patrón de Integración
Sección titulada «Patrón de Integración»Las API generadas admiten dos patrones de integración:
isolatedcrea una función Lambda por operación. Esta es la opción predeterminada y recomendada para las API.sharedcrea un único enrutador Lambda predeterminado y lo reutiliza para cada operación a menos que anule integraciones específicas.
isolated le brinda permisos y configuración más detallados por operación, así como una mejor separación para registros y trazas. shared reduce la probabilidad de encontrar arranques en frío para API de bajo uso.
El patrón de integración se puede cambiar en cualquier momento en CDK actualizando su constructo de API. Por ejemplo, establecer pattern en 'shared' crea una única función en lugar de una por integración:
export class MyApi<...> extends ... {
public static defaultIntegrations = (scope: Construct) => { ... return IntegrationBuilder.rest({ pattern: 'shared', ... }); };}A diferencia de CDK, el patrón de integración está integrado en el módulo generado. Para cambiar el patrón de integración:
- Elimine el módulo de API generado previamente en
packages/common/terraform/src/app/apis - Vuelva a ejecutar el generador que creó su API con el otro patrón de integración (ej.
--integrationPattern=shared)
Con el patrón isolated, el módulo lee las operaciones de un archivo generado:
locals { operations_file = "${path.module}/../../../generated/my-api/operations.json" operations = fileexists(local.operations_file) ? jsondecode(file(local.operations_file)) : {}}Este archivo se genera a partir de su API, por lo que no necesita editarlo manualmente. Agregar una operación al código de aplicación de su API agrega la ruta y la función lambda en el próximo despliegue. Está en .gitignore de forma predeterminada; elimine la entrada si prefiere registrarlo.
Límite de Profundidad de Ruta de REST API en Terraform
Sección titulada «Límite de Profundidad de Ruta de REST API en Terraform»Otorgando Acceso (Solo IAM)
Sección titulada «Otorgando Acceso (Solo IAM)»Puedes otorgar acceso a tu API de la siguiente manera:
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}Las salidas clave del módulo de API que puedes usar para políticas IAM son:
module.my_api.api_execution_arn- Para otorgar permisos execute-api:Invokemodule.my_api.api_arn- El ARN de API Gatewaymodule.my_api.lambda_function_arn- El ARN de la función Lambda
Bundle Target
Sección titulada «Bundle Target»El generador configura automáticamente un target bundle que utiliza Rolldown para crear un paquete de despliegue:
pnpm nx bundle <project-name>yarn nx bundle <project-name>npx nx bundle <project-name>bunx nx bundle <project-name>La configuración de Rolldown se encuentra en rolldown.config.ts, con una entrada por cada bundle a generar. Rolldown gestiona la creación de múltiples bundles en paralelo si están definidos.
Servidor tRPC Local
Sección titulada «Servidor tRPC Local»Puedes usar el target serve para ejecutar un servidor local para tu API, por ejemplo:
pnpm nx serve my-apiyarn nx serve my-apinpx nx serve my-apibunx nx serve my-apiEl punto de entrada para el servidor local es src/local-server.ts.
Esto se recargará automáticamente cuando hagas cambios en tu API.
Invocando tu API tRPC
Sección titulada «Invocando tu API tRPC»Puedes crear un cliente tRPC para invocar tu API de manera type-safe. Si estás llamando a tu API tRPC desde otro backend, puedes usar el cliente en src/client/index.ts, por ejemplo:
import { createMyApiClient } from '@my-scope/my-api';
const client = createMyApiClient({ url: 'https://my-api-url.example.com/' });
await client.echo.query({ message: 'Hello world!' });Si estás llamando a tu API desde un sitio web React, considera usar el generador de Connection para configurar el cliente.
Más Información
Sección titulada «Más Información»Para más información sobre tRPC, consulta la documentación de tRPC.
Conexiones
Sección titulada «Conexiones»Usa el generador connection para integrar este proyecto con otros en tu espacio de trabajo. Las siguientes conexiones involucran este proyecto: