tRPC
tRPC es un framework para construir APIs en TypeScript con seguridad de tipos de extremo a extremo. Con tRPC, las actualizaciones en 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 en AWS CDK o Terraform. El backend generado utiliza AWS Lambda para despliegues 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 con 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 formas:
- 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#trpc-api
- Complete los parámetros requeridos
- Haga clic en
Generate
pnpm nx g @aws/nx-plugin:ts#trpc-api
yarn nx g @aws/nx-plugin:ts#trpc-api
npx nx g @aws/nx-plugin:ts#trpc-api
bunx nx g @aws/nx-plugin:ts#trpc-api
También puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:ts#trpc-api --dry-run
yarn nx g @aws/nx-plugin:ts#trpc-api --dry-run
npx nx g @aws/nx-plugin:ts#trpc-api --dry-run
bunx nx g @aws/nx-plugin:ts#trpc-api --dry-run
Opciones
Sección titulada «Opciones»Parámetro | Tipo | Predeterminado | Descripción |
---|---|---|---|
name Requerido | string | - | The name of the API (required). Used to generate class names and file paths. |
computeType | string | ServerlessApiGatewayRestApi | The type of compute to use to deploy this API. Choose between ServerlessApiGatewayRestApi (default) or ServerlessApiGatewayHttpApi. |
auth | string | IAM | The method used to authenticate with your API. Choose between IAM (default), Cognito or None. |
directory | string | packages | The directory to store the application in. |
iacProvider | string | CDK | The preferred IaC provider |
Salida del Generador
Sección titulada «Salida del Generador»El generador creará la siguiente estructura de proyecto en el directorio <directory>/<api-name>
:
Directorysrc
- init.ts Inicialización del backend tRPC
- router.ts Definición del router tRPC (punto de entrada del handler Lambda)
Directoryschema Definiciones de esquemas usando Zod
- echo.ts Ejemplo de definiciones para entrada y salida del procedimiento “echo”
Directoryprocedures Procedimientos (u operaciones) expuestos por tu API
- echo.ts Procedimiento de ejemplo
Directorymiddleware
- error.ts Middleware para manejo de errores
- logger.ts Middleware para configurar AWS Powertools para logging en Lambda
- tracer.ts Middleware para configurar AWS Powertools para trazado en Lambda
- metrics.ts Middleware para configurar AWS Powertools para métricas en Lambda
- local-server.ts Punto de entrada del adaptador standalone de tRPC para servidor de desarrollo local
Directoryclient
- index.ts Cliente tipado para llamadas máquina-a-máquina a la API
- tsconfig.json Configuración de TypeScript
- project.json Configuración de proyecto y targets de build
Infraestructura
Sección titulada «Infraestructura»Dado que este generador proporciona infraestructura como código basada en tu proveedor de iacProvider
seleccionado, creará un proyecto en packages/common
que incluye los constructos CDK o módulos de Terraform correspondientes.
El proyecto común de infraestructura como código tiene la siguiente estructura:
Directorypackages/common/constructs
Directorysrc
Directoryapp/ Constructos para infraestructura específica de un proyecto/generador
- …
Directorycore/ Constructos genéricos reutilizados por los constructos en
app
- …
- index.ts Punto de entrada que exporta los constructos de
app
- project.json Objetivos de compilación y configuración del proyecto
Directorypackages/common/terraform
Directorysrc
Directoryapp/ Módulos de Terraform para infraestructura específica de un proyecto/generador
- …
Directorycore/ Módulos genéricos reutilizados por los módulos en
app
- …
- project.json Objetivos de compilación y configuración del proyecto
Para implementar tu API, se generan los siguientes archivos:
Directorypackages/common/constructs/src
Directoryapp
Directoryapis
- <project-name>.ts Constructo de CDK para implementar tu API
Directorycore
Directoryapi
- http-api.ts Constructo de CDK para implementar una API HTTP (si seleccionaste implementar una API HTTP)
- rest-api.ts Constructo de CDK para implementar una API REST (si seleccionaste implementar una API REST)
- utils.ts Utilidades para los constructos de API
Directorypackages/common/terraform/src
Directoryapp
Directoryapis
Directory<project-name>
- <project-name>.tf Módulo para implementar tu API
Directorycore
Directoryapi
Directoryhttp-api
- http-api.tf Módulo para implementar una API HTTP (si seleccionaste implementar una API HTTP)
Directoryrest-api
- rest-api.tf Módulo para implementar una API REST (si seleccionaste implementar una API REST)
Implementando tu API tRPC
Sección titulada «Implementando tu API tRPC»En términos generales, las API tRPC constan de un router que delega solicitudes a procedimientos específicos. Cada procedimiento tiene una entrada y salida definidas como esquemas Zod.
Esquema
Sección titulada «Esquema»El directorio src/schema
contiene los tipos compartidos entre tu código cliente y servidor. En este paquete, estos tipos se definen usando Zod, una biblioteca TypeScript-first para declaración y validación de esquemas.
Un esquema de ejemplo podría verse así:
import { z } from 'zod';
// Definición del esquemaexport const UserSchema = z.object({ name: z.string(), height: z.number(), dateOfBirth: z.string().datetime(),});
// Tipo TypeScript correspondienteexport 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 por el código del cliente y servidor, proporcionando un único lugar para actualizar cuando se realizan cambios en las estructuras usadas en tu API.
Los esquemas son validados automáticamente por tu API tRPC en tiempo de ejecución, lo que evita tener que crear lógica de validación manual en el 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 de documentación de Zod.
Router y Procedimientos
Sección titulada «Router y Procedimientos»El punto de entrada de tu API se encuentra en src/router.ts
. Este archivo contiene el handler Lambda que enruta las solicitudes a “procedimientos” basados en la operación invocada. Cada procedimiento define la entrada esperada, la salida y la implementación.
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 echo
de ejemplo se genera en src/procedures/echo.ts
:
export const echo = publicProcedure .input(EchoInputSchema) .output(EchoOutputSchema) .query((opts) => ({ result: opts.input.message }));
Desglosando lo anterior:
publicProcedure
define un método público en la API, incluyendo el middleware configurado ensrc/middleware
. Este middleware incluye integración con AWS Lambda Powertools para logging, trazado y métricas.input
acepta un esquema Zod que define la entrada esperada para la operación. Las solicitudes para esta operación se validan automáticamente contra este esquema.output
acepta un esquema Zod que define la salida esperada. Verás errores de tipo en tu implementación si no devuelves una salida que cumpla con el esquema.query
acepta una función que define la implementación de tu API. Esta implementación recibeopts
, que contiene elinput
pasado a tu operación, así como otro contexto configurado por el middleware, disponible enopts.ctx
. La función pasada aquery
debe devolver una salida que cumpla con el esquema deoutput
.
El uso de query
para definir la implementación indica que la operación no es mutativa. Úsalo para definir métodos de recuperación de datos. Para operaciones mutativas, usa el método mutation
en su lugar.
Si agregas un nuevo procedimiento, asegúrate de registrarlo añadiéndolo al router en src/router.ts
.
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: 'El recurso solicitado no pudo ser encontrado',});
Organizando tus Operaciones
Sección titulada «Organizando tus Operaciones»A medida que tu API crece, puedes querer 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 entonces reciben esta agrupación de operaciones, por ejemplo, invocar la operación listUsers
se vería así:
client.users.list.query();
Logging
Sección titulada «Logging»El logger de AWS Lambda Powertools se configura en src/middleware/logger.ts
, y se puede acceder a él en una implementación de API via opts.ctx.logger
. Puedes usarlo para registrar en CloudWatch Logs, y/o controlar valores adicionales a incluir en cada mensaje de log estructurado. Por ejemplo:
export const echo = publicProcedure .input(...) .output(...) .query(async (opts) => { opts.ctx.logger.info('Operación llamada con input', opts.input);
return ...; });
Para más información sobre el logger, consulta la documentación de AWS Lambda Powertools Logger.
Registro de Métricas
Sección titulada «Registro de Métricas»Las métricas de AWS Lambda Powertools se configuran en src/middleware/metrics.ts
, y se puede acceder a ellas en una implementación de API via opts.ctx.metrics
. Puedes usarlas 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('Invocaciones', 'Count', 1);
return ...; });
Para más información, consulta la documentación de AWS Lambda Powertools Metrics.
Ajuste Fino de Trazado con X-Ray
Sección titulada «Ajuste Fino de Trazado con X-Ray»El tracer de AWS Lambda Powertools se configura en src/middleware/tracer.ts
, y se puede acceder a él en una implementación de API via opts.ctx.tracer
. Puedes usarlo para agregar trazas con AWS X-Ray y obtener insights detallados sobre el rendimiento y flujo de las solicitudes de la API. Por ejemplo:
export const echo = publicProcedure .input(...) .output(...) .query(async (opts) => { const subSegment = opts.ctx.tracer.getSegment()!.addNewSubsegment('MiAlgoritmo'); // ... lógica de mi algoritmo para capturar 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 detalles del usuario que llama a nuestra API en src/middleware/identity.ts
.
Este ejemplo asume que auth
se configuró como IAM
. Para autenticación con Cognito, el middleware de identidad es más directo, extrayendo los claims relevantes del event
.
Primero, definimos lo que agregaremos al contexto:
export interface IIdentityContext { identity?: { sub: string; username: string; };}
Nota que definimos una propiedad adicional opcional al contexto. tRPC se encarga de asegurar que esto esté definido en procedimientos que hayan configurado correctamente este middleware.
Luego, implementamos el middleware mismo. Tiene la siguiente estructura:
export const createIdentityPlugin = () => { const t = initTRPC.context<...>().create(); return t.procedure.use(async (opts) => { // Agrega lógica aquí para ejecutar antes del procedimiento
const response = await opts.next(...);
// Agrega lógica aquí para ejecutar después del procedimiento
return response; });};
En nuestro caso, queremos extraer detalles del usuario de Cognito que llama. Hacemos esto extrayendo el ID de sujeto (o “sub”) del usuario del evento de API Gateway, y recuperando detalles del usuario desde Cognito. La implementación varía ligeramente dependiendo de si el evento fue proporcionado por una REST API o una HTTP API:
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: `No se pudo determinar el usuario llamante`, }); }
const { Users } = await cognito.listUsers({ // Asume que el ID del user pool está configurado en el entorno de Lambda UserPoolId: process.env.USER_POOL_ID!, Limit: 1, Filter: `sub="${sub}"`, });
if (!Users || Users.length !== 1) { throw new TRPCError({ code: 'FORBIDDEN', message: `No se encontró usuario con subjectId ${sub}`, }); }
// Proporciona la identidad a otros procedimientos en el contexto 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: `No se pudo determinar el usuario llamante`, }); }
const { Users } = await cognito.listUsers({ // Asume que el ID del user pool está configurado en el entorno de Lambda UserPoolId: process.env.USER_POOL_ID!, Limit: 1, Filter: `sub="${sub}"`, });
if (!Users || Users.length !== 1) { throw new TRPCError({ code: 'FORBIDDEN', message: `No se encontró usuario con subjectId ${sub}`, }); }
// Proporciona la identidad a otros procedimientos en el contexto return await opts.next({ ctx: { ...opts.ctx, identity: { sub, username: Users[0].Username!, }, }, }); });};
Desplegando tu API tRPC
Sección titulada «Desplegando tu API tRPC»El generador de API tRPC crea código de infraestructura con CDK o Terraform basado en tu iacProvider
seleccionado. Puedes usar esto para desplegar tu API tRPC.
El constructo CDK para desplegar tu API está en la carpeta common/constructs
. Puedes consumirlo en una aplicación CDK, por ejemplo:
import { MyApi } from ':my-scope/common-constructs`;
export class ExampleStack extends Stack { constructor(scope: Construct, id: string) { // Agrega la API a tu stack const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), }); }}
Esto configura la infraestructura de tu API, incluyendo una REST API o HTTP API de AWS API Gateway, funciones AWS Lambda para la lógica de negocio, y autenticación basada en tu método auth
elegido.
Los módulos de Terraform para desplegar tu API están en la carpeta common/terraform
. Puedes usarlos en una configuración de Terraform:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
# Variables de entorno para la función Lambda env = { ENVIRONMENT = var.environment LOG_LEVEL = "INFO" }
# Políticas IAM adicionales si son necesarias additional_iam_policy_statements = [ # Agrega cualquier permiso adicional que necesite tu API ]
tags = local.common_tags}
Esto configura:
- Una función AWS Lambda que sirve todos los procedimientos tRPC
- API Gateway HTTP/REST API como trigger de la función
- Roles y permisos IAM
- Grupo de logs de CloudWatch
- Configuración de trazado X-Ray
- Configuración CORS
El módulo de Terraform proporciona varias salidas que puedes usar:
# Accede al endpoint de la APIoutput "api_url" { value = module.my_api.stage_invoke_url}
# Accede a detalles de la función Lambdaoutput "lambda_function_name" { value = module.my_api.lambda_function_name}
# Accede al rol IAM para otorgar permisos adicionalesoutput "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"
# Configuración CORS personalizada 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}
Integraciones
Sección titulada «Integraciones»Los constructos CDK de la API REST/HTTP están configurados para proporcionar una interfaz type-safe que define integraciones para cada una de tus operaciones.
Los constructos CDK proporcionan soporte completo de integración con seguridad de tipos como se describe a continuación.
Los módulos de Terraform usan el “patrón router” con una única función Lambda que maneja todas las operaciones. No se admiten integraciones type-safe - el módulo crea una función Lambda que maneja todas las solicitudes de la API.
Para integraciones explícitas por operación con Terraform, necesitarías crear manualmente funciones Lambda individuales y rutas de API Gateway. Consulta la sección Integraciones Explícitas para ver ejemplos.
Integraciones por defecto
Sección titulada «Integraciones por defecto»Puedes usar el método estático defaultIntegrations
para utilizar el patrón por defecto, que define una función AWS Lambda individual para cada operación:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
Los módulos de Terraform usan automáticamente el patrón router con una única función Lambda. No se necesita configuración adicional:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
# El módulo crea automáticamente una única función Lambda # que maneja todas las operaciones de la API tags = local.common_tags}
Accediendo a las integraciones
Sección titulada «Accediendo a las integraciones»Puedes acceder a las funciones AWS Lambda subyacentes a través de la propiedad integrations
del constructo de la API, de manera type-safe. Por ejemplo, si tu API define una operación llamada sayHello
y necesitas agregar permisos a esta función, puedes hacerlo así:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
// sayHello está tipado según las operaciones definidas en tu APIapi.integrations.sayHello.handler.addToRolePolicy(new PolicyStatement({ effect: Effect.ALLOW, actions: [...], resources: [...],}));
Con el patrón router de Terraform, solo hay una función Lambda. Puedes acceder a ella a través de los outputs del módulo:
# Otorgar permisos adicionales a la única función Lambdaresource "aws_iam_role_policy" "additional_permissions" { name = "additional-api-permissions" role = module.my_api.lambda_execution_role_name
policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "s3:GetObject", "s3:PutObject" ] Resource = "arn:aws:s3:::my-bucket/*" } ] })}
Personalizando opciones por defecto
Sección titulada «Personalizando opciones por defecto»Si deseas personalizar las opciones usadas al crear la función Lambda para cada integración por defecto, puedes usar el método withDefaultOptions
. Por ejemplo, si quieres que todas tus funciones Lambda residan en una VPC:
const vpc = new Vpc(this, 'Vpc', ...);
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withDefaultOptions({ vpc, }) .build(),});
Para personalizar opciones como configuración de VPC, necesitas editar el módulo de Terraform generado. Por ejemplo, para agregar soporte de VPC a todas las funciones Lambda:
# Agregar variables de VPCvariable "vpc_subnet_ids" { description = "Lista de IDs de subredes VPC para la función Lambda" type = list(string) default = []}
variable "vpc_security_group_ids" { description = "Lista de IDs de grupos de seguridad VPC para la función Lambda" type = list(string) default = []}
# Actualizar el recurso de función Lambdaresource "aws_lambda_function" "api_lambda" { # ... configuración existente ...
# Agregar configuración VPC vpc_config { subnet_ids = var.vpc_subnet_ids security_group_ids = var.vpc_security_group_ids }}
Luego usar el módulo con configuración VPC:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
# Configuración VPC vpc_subnet_ids = [aws_subnet.private_a.id, aws_subnet.private_b.id] vpc_security_group_ids = [aws_security_group.lambda_sg.id]
tags = local.common_tags}
Sobrescribiendo integraciones
Sección titulada «Sobrescribiendo integraciones»También puedes sobrescribir integraciones para operaciones específicas usando el método withOverrides
. Cada override 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 type-safe. Por ejemplo, si quieres sobrescribir una API getDocumentation
para apuntar a documentación alojada en un sitio externo:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), }, }) .build(),});
Notarás que la integración sobrescrita ya no tiene una propiedad handler
cuando se accede a través de api.integrations.getDocumentation
.
Puedes agregar propiedades adicionales a una integración que también estarán tipadas, permitiendo abstraer otros tipos de integración manteniendo el type-safe. Por ejemplo, si creaste una integración S3 para una API REST y luego quieres referenciar el bucket para una operación particular:
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(),});
// Más tarde, quizás en otro archivo, puedes acceder a la propiedad bucket que definimos// de manera type-safeapi.integrations.getFile.bucket.grantRead(...);
No se admite sobrescribir integraciones específicas con módulos de Terraform debido al patrón router. Todas las operaciones son manejadas por una única función Lambda.
Para diferentes tipos de integración por operación, necesitarías implementar integraciones explícitas manualmente (ver sección Integraciones Explícitas).
Sobrescribiendo autorizadores
Sección titulada «Sobrescribiendo autorizadores»También puedes proveer options
en tu integración para sobrescribir opciones de método específicas como autorizadores. Por ejemplo, si deseas usar autenticación con Cognito para tu operación getDocumentation
:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), options: { authorizer: new CognitoUserPoolsAuthorizer(...) // para REST, o HttpUserPoolAuthorizer para HTTP API } }, }) .build(),});
No se admiten sobrescritas de autorizadores por operación con módulos de Terraform. Toda la API usa el método de autenticación especificado al generar la API (IAM, Cognito o None).
Para autorización por operación, necesitarías implementar integraciones explícitas manualmente como se muestra abajo.
Integraciones explícitas
Sección titulada «Integraciones explícitas»Si prefieres, puedes no usar las integraciones por defecto y proveer directamente una para cada operación. Esto es útil si, por ejemplo, cada operación necesita usar un tipo diferente de integración o quieres recibir un error de tipo al agregar nuevas operaciones:
new MyApi(this, 'MyApi', { integrations: { sayHello: { integration: new LambdaIntegration(...), }, getDocumentation: { integration: new HttpIntegration(...), }, },});
Para integraciones explícitas por operación con Terraform, debes modificar el módulo específico de la aplicación generado para reemplazar la integración proxy por defecto con integraciones específicas para cada operación.
Edita packages/common/terraform/src/app/apis/my-api/my-api.tf
:
- Eliminar las rutas proxy por defecto (ej.
resource "aws_apigatewayv2_route" "proxy_routes"
) - Reemplazar la única función Lambda con funciones individuales para cada operación
- Crear integraciones y rutas específicas para cada operación, reusando el mismo paquete ZIP:
# Eliminar la función Lambda única por defecto resource "aws_lambda_function" "api_lambda" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApiHandler" role = aws_iam_role.lambda_execution_role.arn handler = "index.handler" runtime = "nodejs22.x" timeout = 30 # ... resto de la configuración }
# Eliminar la integración proxy por defecto resource "aws_apigatewayv2_integration" "lambda_integration" { api_id = module.http_api.api_id integration_type = "AWS_PROXY" integration_uri = aws_lambda_function.api_lambda.invoke_arn # ... resto de la configuración }
# Eliminar las rutas proxy por defecto resource "aws_apigatewayv2_route" "proxy_routes" { for_each = toset(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]) api_id = module.http_api.api_id route_key = "${each.key} /{proxy+}" target = "integrations/${aws_apigatewayv2_integration.lambda_integration.id}" # ... resto de la configuración }
# Agregar funciones Lambda individuales para cada operación usando el mismo bundle resource "aws_lambda_function" "say_hello_handler" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApi-SayHello" role = aws_iam_role.lambda_execution_role.arn handler = "sayHello.handler" # Handler específico para esta operación runtime = "nodejs22.x" timeout = 30 source_code_hash = data.archive_file.lambda_zip.output_base64sha256
tracing_config { mode = "Active" }
environment { variables = merge({ AWS_CONNECTION_REUSE_ENABLED = "1" }, var.env) }
tags = var.tags }
resource "aws_lambda_function" "get_documentation_handler" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApi-GetDocumentation" role = aws_iam_role.lambda_execution_role.arn handler = "getDocumentation.handler" # Handler específico para esta operación runtime = "nodejs22.x" timeout = 30 source_code_hash = data.archive_file.lambda_zip.output_base64sha256
tracing_config { mode = "Active" }
environment { variables = merge({ AWS_CONNECTION_REUSE_ENABLED = "1" }, var.env) }
tags = var.tags }
# Agregar integraciones específicas para cada operación 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" }
# Agregar rutas específicas para cada operación 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" }
# Agregar permisos Lambda para cada función 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}/*/*" }
# Eliminar la función Lambda única por defecto resource "aws_lambda_function" "api_lambda" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApiHandler" role = aws_iam_role.lambda_execution_role.arn handler = "index.handler" runtime = "nodejs22.x" timeout = 30 # ... resto de la configuración }
# Eliminar la integración proxy por defecto resource "aws_apigatewayv2_integration" "lambda_integration" { api_id = module.http_api.api_id integration_type = "AWS_PROXY" integration_uri = aws_lambda_function.api_lambda.invoke_arn # ... resto de la configuración }
# Eliminar las rutas proxy por defecto resource "aws_apigatewayv2_route" "proxy_routes" { for_each = toset(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]) api_id = module.http_api.api_id route_key = "${each.key} /{proxy+}" target = "integrations/${aws_apigatewayv2_integration.lambda_integration.id}" # ... resto de la configuración }
# Agregar funciones Lambda individuales para cada operación usando el mismo bundle resource "aws_lambda_function" "say_hello_handler" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApi-SayHello" role = aws_iam_role.lambda_execution_role.arn handler = "sayHello.handler" # Handler específico para esta operación runtime = "nodejs22.x" timeout = 30 source_code_hash = data.archive_file.lambda_zip.output_base64sha256
tracing_config { mode = "Active" }
environment { variables = merge({ AWS_CONNECTION_REUSE_ENABLED = "1" }, var.env) }
tags = var.tags }
resource "aws_lambda_function" "get_documentation_handler" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApi-GetDocumentation" role = aws_iam_role.lambda_execution_role.arn handler = "getDocumentation.handler" # Handler específico para esta operación runtime = "nodejs22.x" timeout = 30 source_code_hash = data.archive_file.lambda_zip.output_base64sha256
tracing_config { mode = "Active" }
environment { variables = merge({ AWS_CONNECTION_REUSE_ENABLED = "1" }, var.env) }
tags = var.tags }
# Agregar recursos y métodos específicos para cada operación 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" }
# Actualizar deployment para depender de nuevas integraciones~ 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, ])) } }
# Agregar permisos Lambda para cada función 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}/*/*" }
Patrón Router
Sección titulada «Patrón Router»Si prefieres desplegar una única función Lambda para manejar todas las solicitudes de la API, puedes modificar libremente el método defaultIntegrations
de tu API para crear una sola función en lugar de una por integración:
export class MyApi<...> extends ... {
public static defaultIntegrations = (scope: Construct) => { const router = new Function(scope, 'RouterHandler', { ... }); return IntegrationBuilder.rest({ ... defaultIntegrationOptions: {}, buildDefaultIntegration: (op) => { return { // Referenciar el mismo router lambda handler en cada integración integration: new LambdaIntegration(router), }; }, }); };}
Puedes modificar el código de otras formas si prefieres, por ejemplo definiendo la función router
como parámetro de defaultIntegrations
en lugar de construirla dentro del método.
Los módulos de Terraform usan automáticamente el patrón router - este es el enfoque por defecto y único soportado. El módulo generado crea una única función Lambda que maneja todas las operaciones de la API.
Simplemente instancia el módulo por defecto para obtener el patrón router:
# Patrón router por defecto - única función Lambda para todas las operacionesmodule "my_api" { source = "../../common/terraform/src/app/apis/my-api"
# Una única función Lambda maneja todas las operaciones automáticamente tags = local.common_tags}
Otorgando Acceso (Solo IAM)
Sección titulada «Otorgando Acceso (Solo IAM)»Si seleccionaste usar autenticación IAM
, puedes otorgar acceso a tu API:
api.grantInvokeAccess(myIdentityPool.authenticatedRole);
# Crea una política IAM para permitir invocar la APIresource "aws_iam_policy" "api_invoke_policy" { name = "MyApiInvokePolicy" description = "Política para permitir invocar la API tRPC"
policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = "execute-api:Invoke" Resource = "${module.my_api.api_execution_arn}/*/*" } ] })}
# Adjunta la política a un rol IAM (ej. para usuarios autenticados)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}
# O adjunta a un rol existente por nombreresource "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
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 run @my-scope/my-api:serve
yarn nx run @my-scope/my-api:serve
npx nx run @my-scope/my-api:serve
bunx nx run @my-scope/my-api:serve
El 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 tipada. 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: '¡Hola mundo!' });
Si estás llamando a tu API desde un sitio React, considera usar el generador Conexión de API 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.