tRPC
tRPC è un framework per la creazione di API in TypeScript con type safety end-to-end. Utilizzando tRPC, gli aggiornamenti agli input e output delle operazioni API si riflettono immediatamente nel codice client e sono visibili nel tuo IDE senza la necessità di ricostruire il progetto.
Il generatore di API tRPC crea una nuova API tRPC con configurazione dell’infrastruttura AWS CDK o Terraform. Il backend generato utilizza AWS Lambda per il deployment serverless, esposto tramite un’API AWS API Gateway, e include la validazione dello schema utilizzando Zod. Configura AWS Lambda Powertools per l’osservabilità, inclusi logging, tracing AWS X-Ray e Cloudwatch Metrics.
Utilizzo
Sezione intitolata “Utilizzo”Generare un’API tRPC
Sezione intitolata “Generare un’API tRPC”Puoi generare una nuova API tRPC in due modi:
Esegui questo generatore@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- Installa il Nx Console VSCode Plugin se non l'hai già fatto
- Apri la console Nx in VSCode
- Clicca su
Generate (UI)nella sezione "Common Nx Commands" - Cerca
@aws/nx-plugin - ts#api - Compila i parametri richiesti
- Clicca su
Generate
Componi il tuo comando9
Obbligatorio
Opzioni
Sezione intitolata “Opzioni”nameObbligatoriostringIl nome dell'API (obbligatorio). Utilizzato per generare i nomi delle classi e i percorsi dei file.
frameworkenumPredefinito:trpcIl framework API da utilizzare.
trpcsmithyintegrationPatternenumPredefinito:isolatedCome vengono generate le integrazioni API Gateway per l'API. Scegli tra isolated (predefinito) e shared.
isolatedsharedauthenumPredefinito:iamIl metodo utilizzato per autenticare con la tua API. Scegli tra iam (predefinito), cognito o custom.
iamcognitocustomdirectorystringPredefinito:packagesLa directory in cui memorizzare l'applicazione.
iacenumPredefinito:inheritIl provider IaC preferito. Per impostazione predefinita viene ereditato dalla selezione iniziale.
inheritcdkterraforminfraenumPredefinito:rest-lambdaIl tipo di infrastruttura da utilizzare per distribuire questa API.
rest-lambdahttp-lambdanonesubDirectorystringLa sottodirectory in cui viene posizionato il progetto. Per impostazione predefinita corrisponde al nome del progetto.
preferInstallDependenciesbooleanPredefinito:trueSe preferire l'installazione delle dipendenze dopo l'esecuzione del generatore. Impostare su false per rimandare l'installazione quando si eseguono più generatori in batch (l'installazione viene comunque eseguita se necessaria affinché i generatori successivi possano calcolare il grafo dei progetti Nx); installare una volta alla fine.
Output del Generatore
Sezione intitolata “Output del Generatore”Il generatore creerà la seguente struttura di progetto nella directory <directory>/<api-name>:
Directorysrc
- 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
Directoryschema 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)
Directoryprocedures Procedures (or operations) exposed by your API
- echo.ts Example procedure
Directorymiddleware
- 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
Directoryclient
- 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
Infrastruttura
Sezione intitolata “Infrastruttura”Poiché questo generatore fornisce infrastruttura come codice basata sul tuo iac scelto, creerà un progetto in packages/common che include i costrutti CDK o i moduli Terraform pertinenti.
Il progetto comune di infrastruttura come codice è strutturato come segue:
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
Per il deployment della tua API, vengono generati i seguenti file:
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)
Architettura
Sezione intitolata “Architettura”L’applicazione distribuita ha la seguente architettura: un’API di API Gateway davanti a una funzione Lambda che esegue il tuo handler.
Le REST API includono una Web ACL AWS WAFv2 davanti allo stage di API Gateway con il set di regole predefinito gestito da AWS abilitato.
Le HTTP API non supportano direttamente WAF — se hai bisogno della protezione WAF, scegli REST API oppure posiziona l’HTTP API dietro una distribuzione CloudFront.
Implementare la tua API tRPC
Sezione intitolata “Implementare la tua API tRPC”Ad alto livello, le API tRPC consistono in un router che delega le richieste a procedure specifiche. Ogni procedura ha un input e un output, definiti come schema Zod.
La directory src/schema contiene i tipi condivisi tra il codice client e server. In questo package, questi tipi sono definiti utilizzando Zod, una libreria di dichiarazione e validazione di schemi TypeScript-first.
Un esempio di schema potrebbe apparire come segue:
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>;Dato lo schema sopra, il tipo User è equivalente al seguente TypeScript:
interface User { name: string; height: number; dateOfBirth: string;}Gli schemi sono condivisi sia dal codice server che client, fornendo un unico posto da aggiornare quando si apportano modifiche alle strutture utilizzate nella tua API.
Gli schemi vengono automaticamente validati dalla tua API tRPC a runtime, il che evita di dover creare manualmente logica di validazione personalizzata nel tuo backend.
Zod fornisce potenti utility per combinare o derivare schemi come .merge, .pick, .omit e altro. Puoi trovare maggiori informazioni sul sito di documentazione di Zod.
Router e Procedure
Sezione intitolata “Router e Procedure”Il tuo router tRPC è definito in src/router.ts, che registra tutte le procedure. Ogni procedura definisce l’input, l’output e l’implementazione previsti. Il punto di ingresso dell’handler Lambda è in src/handler.ts, che inoltra le richieste al tuo router.
Il router di esempio generato per te ha una singola operazione, chiamata echo:
import { echo } from './procedures/echo.js';
export const appRouter = router({ echo,});La procedura di esempio echo viene generata per te in src/procedures/echo.ts:
export const echo = publicProcedure .input(EchoInputSchema) .output(EchoOutputSchema) .query((opts) => ({ message: opts.input.message }));Per analizzare quanto sopra:
publicProceduredefinisce un metodo pubblico sull’API, incluso il middleware configurato insrc/middleware. Questo middleware include l’integrazione di AWS Lambda Powertools per logging, tracing e metriche.inputaccetta uno schema Zod che definisce l’input previsto per l’operazione. Le richieste inviate per questa operazione vengono automaticamente validate rispetto a questo schema.outputaccetta uno schema Zod che definisce l’output previsto per l’operazione. Vedrai errori di tipo nella tua implementazione se non restituisci un output conforme allo schema.queryaccetta una funzione che definisce l’implementazione per la tua API. Questa implementazione riceveopts, che contiene l’inputpassato alla tua operazione, così come altro contesto configurato dal middleware, disponibile inopts.ctx. La funzione passata aquerydeve restituire un output conforme allo schemaoutput.
L’uso di query per definire l’implementazione indica che l’operazione non è mutativa. Usalo per definire metodi per recuperare dati. Per implementare un’operazione mutativa, usa invece il metodo mutation.
Se aggiungi una nuova procedura, assicurati di registrarla aggiungendola al router in src/router.ts.
Subscriptions (Streaming)
Sezione intitolata “Subscriptions (Streaming)”Le subscriptions tRPC ti permettono di trasmettere dati dal server al client utilizzando Server-Sent Events (SSE). Quando selezioni rest-lambda come tipo di compute, il generatore configura automaticamente l’infrastruttura richiesta per lo streaming, così come un handler Lambda di streaming e l’helper di schema ZodAsyncIterable.
Per definire una procedura di subscription, usa il metodo .subscription con una funzione generatore asincrona. Usa l’helper ZodAsyncIterable da src/schema/z-async-iterable.ts per definire lo schema di output:
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 subscription nel tuo router proprio come qualsiasi altra procedura:
export const appRouter = router({ echo, myStream,});L’infrastruttura generata utilizza un handler Lambda di streaming con ResponseTransferMode.STREAM in API Gateway per tutte le operazioni REST API, il che consente alle subscriptions di funzionare insieme a query e mutations regolari.
Personalizzare la tua API tRPC
Sezione intitolata “Personalizzare la tua API tRPC”Nella tua implementazione, puoi restituire risposte di errore ai client lanciando un TRPCError. Questi accettano un code che indica il tipo di errore, per esempio:
throw new TRPCError({ code: 'NOT_FOUND', message: 'The requested resource could not be found',});Organizzare le Tue Operazioni
Sezione intitolata “Organizzare le Tue Operazioni”Man mano che la tua API cresce, potresti voler raggruppare operazioni correlate insieme.
Puoi raggruppare operazioni insieme utilizzando router nidificati, per esempio:
import { getUser } from './procedures/users/get.js';import { listUsers } from './procedures/users/list.js';
const appRouter = router({ users: router({ get: getUser, list: listUsers, }), ...})I client ricevono quindi questo raggruppamento di operazioni, per esempio invocare l’operazione listUsers in questo caso potrebbe apparire come segue:
client.users.list.query();Logging
Sezione intitolata “Logging”Il logger AWS Lambda Powertools è configurato in src/middleware/logger.ts, e può essere accessibile in un’implementazione API tramite opts.ctx.logger. Puoi usarlo per registrare su CloudWatch Logs, e/o controllare valori aggiuntivi da includere in ogni messaggio di log strutturato. Per esempio:
export const echo = publicProcedure .input(...) .output(...) .query(async (opts) => { opts.ctx.logger.info('Operation called with input', opts.input);
return ...; });Per maggiori informazioni sul logger, consulta la documentazione di AWS Lambda Powertools Logger.
Registrare Metriche
Sezione intitolata “Registrare Metriche”Le metriche AWS Lambda Powertools sono configurate in src/middleware/metrics.ts, e possono essere accessibili in un’implementazione API tramite opts.ctx.metrics. Puoi usarle per registrare metriche in CloudWatch senza la necessità di importare e utilizzare l’AWS SDK, per esempio:
export const echo = publicProcedure .input(...) .output(...) .query(async (opts) => { opts.ctx.metrics.addMetric('Invocations', 'Count', 1);
return ...; });Per maggiori informazioni, consulta la documentazione di AWS Lambda Powertools Metrics.
Ottimizzare il Tracing X-Ray
Sezione intitolata “Ottimizzare il Tracing X-Ray”Il tracer AWS Lambda Powertools è configurato in src/middleware/tracer.ts, e può essere accessibile in un’implementazione API tramite opts.ctx.tracer. Puoi usarlo per aggiungere tracce con AWS X-Ray per fornire informazioni dettagliate sulle prestazioni e il flusso delle richieste API. Per esempio:
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 ...; });Per maggiori informazioni, consulta la documentazione di AWS Lambda Powertools Tracer.
Implementare Middleware Personalizzato
Sezione intitolata “Implementare Middleware Personalizzato”Puoi aggiungere valori aggiuntivi al contesto fornito alle procedure implementando middleware.
Come esempio, implementiamo un middleware per estrarre alcuni dettagli sull’utente chiamante dalla nostra API in src/middleware/identity.ts.
Questo esempio illustra il middleware di identità per l’autenticazione IAM. Cerchiamo il chiamante in Cognito utilizzando il sub estratto dall’evento API Gateway.
La ricerca utilizza il client Cognito Identity Provider, che non è una dipendenza di un’API tRPC generata. Installalo prima nel tuo progetto 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-apiPrima, definiamo cosa aggiungeremo al contesto:
export interface IIdentityContext { identity?: { sub: string; username: string; };}Nota che definiamo una proprietà aggiuntiva opzionale al contesto. tRPC gestisce l’assicurazione che questa sia definita nelle procedure che hanno configurato correttamente questo middleware.
Successivamente, implementeremo il middleware stesso. Questo ha la seguente struttura:
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; });};Nel nostro caso, vogliamo estrarre dettagli sull’utente Cognito chiamante. Lo faremo estraendo l’ID soggetto dell’utente (o “sub”) dall’evento API Gateway e recuperando i dettagli dell’utente da Cognito. L’implementazione varia a seconda che l’evento sia stato fornito alla nostra funzione da una REST API o da un’HTTP API:
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!, }, }, }); });};Quando effettui il deploy con auth: 'cognito', l’autorizzatore Cognito di API Gateway verifica il JWT che il chiamante fornisce nell’header Authorization e posiziona i claim verificati sull’evento Lambda. Il nostro middleware legge semplicemente quei claim — nessuna chiamata AWS SDK extra, nessuna verifica JWT manuale.
Prima, definiamo cosa aggiungeremo al contesto:
export interface IIdentityContext { identity?: { sub: string; username: string; };}Nota che definiamo una proprietà aggiuntiva opzionale sul contesto. tRPC gestisce l’assicurazione che questa sia definita nelle procedure che hanno configurato correttamente questo middleware.
Successivamente, il middleware stesso. Il tipo di evento e la posizione dei claim differiscono tra una REST API e un’HTTP API, quindi l’implementazione dipende dal tuo infra selezionato:
L’autorizzatore Cognito User Pools di una REST API posiziona i claim in 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, }, }, }); });};L’autorizzatore JWT di un’HTTP API fornisce un evento payload-v2 i cui claim si trovano un livello più in profondità, in event.requestContext.authorizer.jwt.claims. Il contesto deve essere tipizzato su APIGatewayProxyEventV2WithJWTAuthorizer per corrispondere a quello utilizzato dal publicProcedure generato — altrimenti .concat() fallisce con l’errore Context mismatch di 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, }, }, }); });};Puoi quindi mescolare il plugin in qualsiasi procedura che necessita dell’identità del chiamante:
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, }));Effettuare il Deploy della tua API tRPC
Sezione intitolata “Effettuare il Deploy della tua API tRPC”Il generatore di API tRPC crea infrastruttura come codice CDK o Terraform in base al tuo iac selezionato. Puoi usarlo per effettuare il deploy della tua API tRPC.
Il costrutto CDK per il deploy della tua API si trova nella cartella common/constructs. Puoi utilizzarlo in un’applicazione CDK, per esempio:
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, }); }}Il costrutto UserIdentity può essere generato utilizzando il generatore ts#website#auth.
Questo configura l’infrastruttura della tua API, inclusa un’API AWS API Gateway REST o HTTP, funzioni AWS Lambda per la logica di business e autenticazione basata sul tuo metodo auth scelto.
I moduli Terraform per il deploy della tua API sono in the common/terraform folder. Puoi usare questo in una configurazione Terraform.
Il modulo API prepara il suo zip di deployment Lambda in un bucket S3 di asset condiviso — vedi la guida all’infrastruttura Terraform per i dettagli. Istanzia il modulo core/asset-bucket una volta per deployment e passa il suo output bucket_name in ogni modulo API / Lambda tramite l’input 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}Puoi configurare il Cognito User Pool e Client utilizzando le risorse o i moduli Terraform appropriati.
Questo configura:
- Una funzione AWS Lambda che serve tutte le procedure tRPC
- API Gateway HTTP/REST API come trigger della funzione
- Ruoli e permessi IAM
- Gruppo di log CloudWatch
- Configurazione del tracing X-Ray
- Configurazione CORS
Il modulo Terraform fornisce diversi output che puoi utilizzare:
# 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}Puoi personalizzare le impostazioni CORS passando variabili al modulo:
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}Per le REST API, il costrutto generato associa un Web ACL AWS WAFv2 allo stage di API Gateway per impostazione predefinita. Il Web ACL utilizza il set di regole predefinito gestito da AWS (AWSManagedRulesCommonRuleSet e AWSManagedRulesKnownBadInputsRuleSet), fornendo protezione contro exploit web comuni inclusa la OWASP Top 10. I log delle richieste WAF vengono scritti in un gruppo di CloudWatch Logs.
Puoi modificare il costrutto rest-api generato per aggiungere, rimuovere o regolare le regole (ad esempio, per aggiungere regole basate sulla frequenza o gruppi di regole gestite aggiuntivi).
Per disattivare (ad esempio, per collegare il tuo Web ACL), imposta enableWaf su false:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), enableWaf: false,});Per disattivare (ad esempio, per collegare il tuo Web ACL), imposta enable_waf su false:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name enable_waf = false}Logging degli Accessi
Sezione intitolata “Logging degli Accessi”Per le REST API, l’infrastruttura generata abilita la registrazione degli accessi per impostazione predefinita, scrivendo una riga JSON strutturata per richiesta in un gruppo CloudWatch Logs dedicato. Il gruppo di log è crittografato con una chiave KMS gestita dal cliente e conservato per un anno.
API Gateway scrive i log di accesso utilizzando un ruolo CloudWatch Logs a livello di account. Questo ruolo è configurato sull’impostazione AWS::ApiGateway::Account, che è un singleton per regione per account — esiste un solo ruolo per ogni REST API nella regione. Per gestire questo in modo sicuro tra più stack distribuiti indipendentemente, l’infrastruttura generata:
- Crea un ruolo CloudWatch Logs condiviso e lo configura sull’account solo quando non è già impostato un ruolo funzionante, in modo che le distribuzioni non sovrascrivano mai un ruolo di proprietà di un altro stack.
- Lascia intatta l’impostazione dell’account durante lo smantellamento, in modo che la distruzione di uno stack non disabiliti mai la registrazione per altre REST API nella regione.
Il ruolo dell’account è gestito dal costrutto ApiGatewayAccount, un singleton con ambito stack risolto tramite ApiGatewayAccount.ensure(scope). Ogni stage della REST API dipende da esso e il ruolo è configurato da una risorsa personalizzata supportata da Lambda.
Il formato del log di accesso è impostato dal costrutto RestApi che la tua API estende. Per personalizzarlo, passa deployOptions a super nel file generato packages/common/constructs/src/app/apis/my-api.ts, mantenendo il tracingEnabled che il costrutto già imposta:
super(scope, id, { apiName: 'MyApi', // ... deployOptions: { tracingEnabled: true, accessLogFormat: AccessLogFormat.clf(), }, ...props,});AccessLogFormat è importato da aws-cdk-lib/aws-apigateway. Tutto ciò che lasci non impostato mantiene il valore predefinito del costrutto — un formato JSON con i campi standard.
Il ruolo dell’account è gestito dal modulo core/api/api-gateway-account, che viene istanziato dal modulo API generato. Configura l’account in modo idempotente e non viene mai reimpostato con terraform destroy.
Puoi personalizzare il formato del log di accesso modificando il blocco access_log_settings sulla risorsa aws_api_gateway_stage nel modulo API generato.
Integrazioni
Sezione intitolata “Integrazioni”I costrutti CDK REST/HTTP API sono configurati per fornire un’interfaccia type-safe per definire integrazioni per ciascuna delle tue operazioni.
Integrazioni Predefinite
Sezione intitolata “Integrazioni Predefinite”Puoi utilizzare il metodo statico defaultIntegrations per utilizzare il pattern predefinito, che definisce una funzione AWS Lambda individuale per ogni operazione:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});Il modulo generato definisce già le integrazioni predefinite per il pattern con cui l’API è stata generata, quindi non è necessaria alcuna configurazione aggiuntiva:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
tags = local.common_tags}Con il pattern predefinito isolated, questo crea una funzione Lambda per operazione.
Accesso alle Integrazioni
Sezione intitolata “Accesso alle Integrazioni”Puoi accedere alle funzioni AWS Lambda sottostanti tramite la proprietà integrations del costrutto API, in modo type-safe. Ad esempio, se la tua API definisce un’operazione chiamata sayHello e devi aggiungere alcune autorizzazioni a questa funzione, puoi farlo come segue:
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 la tua API utilizza il pattern shared, il router Lambda condiviso è esposto come api.integrations.$router:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
api.integrations.$router.handler.addEnvironment('LOG_LEVEL', 'DEBUG');Nota che $router non è più disponibile se sovrascrivi ogni operazione tramite withOverrides, poiché nessuna operazione rimane ad utilizzare l’integrazione router predefinita.
Con il pattern isolated, gli output del modulo sono mappe indicizzate per nome di operazione, quindi puoi raggiungere le risorse di una singola operazione. Ad esempio, per concedere autorizzazioni extra alla funzione Lambda di un’operazione:
# 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/*" } ] })}Per concedere le stesse autorizzazioni a ogni operazione, itera l’output 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/*" } ] })}Il modulo espone anche lambda_function_names, lambda_function_arns, lambda_invoke_arns, integration_ids e lambda_log_group_names come mappe indicizzate per nome di operazione. Con il pattern shared vengono invece esposti gli output singolari equivalenti (lambda_execution_role_name, lambda_function_name, …), poiché c’è solo una funzione.
Le autorizzazioni di cui ogni operazione ha bisogno è meglio passarle al modulo, che le applica al ruolo di ogni funzione:
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/*"] } ]}Personalizzazione delle Opzioni Predefinite
Sezione intitolata “Personalizzazione delle Opzioni Predefinite”Se desideri personalizzare le opzioni utilizzate durante la creazione della funzione Lambda per ogni integrazione predefinita, puoi utilizzare il metodo withDefaultOptions. Ad esempio, se desideri che tutte le tue funzioni Lambda risiedano in un Vpc:
const vpc = new Vpc(this, 'Vpc', ...);
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withDefaultOptions({ vpc, }) .build(),});La configurazione VPC è già supportata dal modulo generato — imposta enable_vpc insieme a vpc_id e subnet_ids, e il modulo distribuisce ogni funzione Lambda nel tuo VPC dietro un security group condiviso che crea per te:
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}Per opzioni che il modulo non espone, modifica direttamente la risorsa aws_lambda_function nel modulo Terraform generato. Con il pattern isolated quella singola risorsa è dichiarata for_each = local.operations, quindi una modifica lì si applica a ogni operazione.
Personalizzazione delle Opzioni per Operazione
Sezione intitolata “Personalizzazione delle Opzioni per Operazione”Per personalizzare le opzioni utilizzate per creare l’integrazione predefinita per operazioni specifiche (senza influenzare le altre), puoi utilizzare il metodo withOperationOptions. Ad esempio, se desideri aumentare il timeout della funzione Lambda solo per un’operazione:
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({ ... }));Le opzioni che specifichi vengono unite alle opzioni di integrazione predefinite (e a qualsiasi opzione impostata tramite withDefaultOptions). Nota che non puoi specificare opzioni per operazioni che hai sostituito tramite withOverrides, poiché queste non utilizzano più l’integrazione predefinita.
Incontrerai un errore di tipo se la stessa operazione è targetizzata sia da withOperationOptions che da withOverrides, indipendentemente dall’ordine in cui li chiami.
Con il pattern isolated la risorsa della funzione Lambda è già per operazione, quindi le opzioni possono essere variate per nome di operazione. Ad esempio, per dare a un’operazione un timeout più lungo, modifica la risorsa aws_lambda_function nel modulo generato:
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}Override delle Integrazioni
Sezione intitolata “Override delle Integrazioni”Puoi anche sovrascrivere le integrazioni per operazioni specifiche utilizzando il metodo withOverrides. Ogni override deve specificare una proprietà integration che è tipizzata al costrutto di integrazione CDK appropriato per l’API HTTP o REST. Il metodo withOverrides è anche type-safe. Ad esempio, se desideri sovrascrivere un’API getDocumentation per puntare alla documentazione ospitata da un sito web esterno, potresti ottenere questo come segue:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), }, }) .build(),});Noterai anche che l’integrazione sovrascritta non ha più una proprietà handler quando vi accedi tramite api.integrations.getDocumentation.
Puoi aggiungere proprietà aggiuntive a un’integrazione che saranno anche tipizzate di conseguenza, consentendo ad altri tipi di integrazione di essere astratti ma rimanere type-safe, ad esempio se hai creato un’integrazione S3 per un’API REST e successivamente desideri fare riferimento al bucket per una particolare operazione, puoi farlo come segue:
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(...);Per puntare un’operazione specifica a un tipo di integrazione diverso, escludila dal for_each predefinito e dichiara la sua integrazione separatamente. Ad esempio, per servire getDocumentation da un sito web esterno:
# 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}"}Override degli Authorizer
Sezione intitolata “Override degli Authorizer”Puoi anche fornire options nella tua integrazione per sovrascrivere particolari opzioni del metodo come gli authorizer, ad esempio se desideri utilizzare l’autenticazione Cognito per la tua operazione 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(),});L’autorizzazione è impostata sulla route (HTTP API) o sul metodo (REST API) per ogni operazione, quindi può essere variata per nome di operazione. Ad esempio, per lasciare un’operazione non autenticata su un’API HTTP:
resource "aws_apigatewayv2_route" "operation_routes" { for_each = local.operations
# ... rest of configuration
authorization_type = each.key == "getDocumentation" ? "NONE" : "AWS_IAM"}Per un’API REST autenticata con IAM, aggiungi anche una dichiarazione di policy delle risorse che consente l’accesso non autenticato al percorso di quell’operazione.
Integrazioni Esplicite
Sezione intitolata “Integrazioni Esplicite”Se preferisci, puoi scegliere di non utilizzare le integrazioni predefinite e invece fornirne direttamente una per ogni operazione. Questo è utile se, ad esempio, ogni operazione deve utilizzare un tipo diverso di integrazione o desideri ricevere un errore di tipo quando aggiungi nuove operazioni:
new MyApi(this, 'MyApi', { integrations: { sayHello: { integration: new LambdaIntegration(...), }, getDocumentation: { integration: new HttpIntegration(...), }, },});Sostituisci il for_each utilizzato dal pattern isolated con istanziazioni esplicite delle funzioni Lambda, integrazioni e autorizzazioni per ogni operazione.
Pattern di Integrazione
Sezione intitolata “Pattern di Integrazione”Le API generate supportano due pattern di integrazione:
isolatedcrea una funzione Lambda per operazione. Questa è l’opzione predefinita e consigliata per le API.sharedcrea un singolo router Lambda predefinito e lo riutilizza per ogni operazione a meno che non si sovrascrivano integrazioni specifiche.
isolated ti offre autorizzazioni e configurazione più granulari per operazione, oltre a una migliore separazione per log e tracce. shared riduce la probabilità di incontrare cold-start per API a basso utilizzo.
Il pattern di integrazione può essere modificato in qualsiasi momento in CDK aggiornando il tuo costrutto API. Ad esempio, impostando pattern su 'shared' si crea una singola funzione invece di una per integrazione:
export class MyApi<...> extends ... {
public static defaultIntegrations = (scope: Construct) => { ... return IntegrationBuilder.rest({ pattern: 'shared', ... }); };}A differenza di CDK, il pattern di integrazione è incorporato nel modulo generato. Per cambiare il pattern di integrazione:
- Elimina il modulo API generato precedentemente in
packages/common/terraform/src/app/apis - Riesegui il generatore che ha creato la tua API con l’altro pattern di integrazione (es.
--integrationPattern=shared)
Con il pattern isolated, il modulo legge le operazioni da un file generato:
locals { operations_file = "${path.module}/../../../generated/my-api/operations.json" operations = fileexists(local.operations_file) ? jsondecode(file(local.operations_file)) : {}}Questo file è generato dalla tua API, quindi non è necessario modificarlo manualmente. Aggiungere un’operazione al codice dell’applicazione API aggiunge la route e la funzione lambda al prossimo deploy. È .gitignored per impostazione predefinita; rimuovi la voce se preferisci includerlo nel controllo versione.
Terraform REST API Path Depth Limit
Sezione intitolata “Terraform REST API Path Depth Limit”Concedere l’Accesso (Solo IAM)
Sezione intitolata “Concedere l’Accesso (Solo IAM)”Puoi concedere l’accesso alla tua API come segue:
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}Gli output chiave dal modulo API che puoi utilizzare per le policy IAM sono:
module.my_api.api_execution_arn- Per concedere permessi execute-api:Invokemodule.my_api.api_arn- L’ARN di API Gatewaymodule.my_api.lambda_function_arn- L’ARN della funzione Lambda
Bundle Target
Sezione intitolata “Bundle Target”Il generatore configura automaticamente un target bundle che utilizza Rolldown per creare un pacchetto di distribuzione:
pnpm nx bundle <project-name>yarn nx bundle <project-name>npx nx bundle <project-name>bunx nx bundle <project-name>La configurazione di Rolldown si trova in rolldown.config.ts, con una voce per ogni bundle da generare. Rolldown gestisce la creazione di più bundle in parallelo se definiti.
Server tRPC Locale
Sezione intitolata “Server tRPC Locale”Puoi utilizzare il target serve per eseguire un server locale per la tua API, per esempio:
pnpm nx serve my-apiyarn nx serve my-apinpx nx serve my-apibunx nx serve my-apiIl punto di ingresso per il server locale è src/local-server.ts.
Questo si ricaricherà automaticamente quando apporti modifiche alla tua API.
Invocare la tua API tRPC
Sezione intitolata “Invocare la tua API tRPC”Puoi creare un client tRPC per invocare la tua API in modo type-safe. Se stai chiamando la tua API tRPC da un altro backend, puoi utilizzare il client in src/client/index.ts, per esempio:
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 stai chiamando la tua API da un sito web React, considera l’utilizzo del generatore Connection per configurare il client.
Maggiori Informazioni
Sezione intitolata “Maggiori Informazioni”Per maggiori informazioni su tRPC, consulta la documentazione di tRPC.
Connessioni
Sezione intitolata “Connessioni”Usa il generatore connection per integrare questo progetto con altri nel tuo workspace. Le seguenti connessioni coinvolgono questo progetto: