tRPC
tRPC è un framework per costruire 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 bisogno di ricompilare il progetto.
Il generatore di API tRPC crea una nuova API tRPC con configurazione dell’infrastruttura AWS CDK. Il backend generato utilizza AWS Lambda per il deployment serverless e include la validazione degli schemi tramite Zod. Configura AWS Lambda Powertools per l’osservabilità, inclusi logging, tracciamento AWS X-Ray e metriche Cloudwatch.
Utilizzo
Sezione intitolata “Utilizzo”Genera un’API tRPC
Sezione intitolata “Genera un’API tRPC”Puoi generare una nuova API tRPC in due modi:
- 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#trpc-api
- Compila i parametri richiesti
- Clicca su
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
Puoi anche eseguire una prova per vedere quali file verrebbero modificati
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
Opzioni
Sezione intitolata “Opzioni”Parametro | Tipo | Predefinito | Descrizione |
---|---|---|---|
name Obbligatorio | 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. |
Output del Generatore
Sezione intitolata “Output del Generatore”Il generatore creerà la seguente struttura del progetto nella directory <directory>/<api-name>
:
Directorysrc
- init.ts Inizializzazione backend tRPC
- router.ts Definizione router tRPC (punto d’ingresso API per handler Lambda)
Directoryschema Definizioni degli schemi usando Zod
- echo.ts Esempio di definizioni per input e output della procedura “echo”
Directoryprocedures Procedure (o operazioni) esposte dalla tua API
- echo.ts Procedura di esempio
Directorymiddleware
- error.ts Middleware per la gestione degli errori
- logger.ts middleware per configurare AWS Powertools per il logging Lambda
- tracer.ts middleware per configurare AWS Powertools per il tracciamento Lambda
- metrics.ts middleware per configurare AWS Powertools per le metriche Lambda
- local-server.ts Punto d’ingresso adattatore standalone tRPC per server di sviluppo locale
Directoryclient
- index.ts Client type-safe per chiamate API machine-to-machine
- tsconfig.json Configurazione TypeScript
- project.json Configurazione progetto e target di build
Il generatore creerà anche costrutti CDK utilizzabili per deployare la tua API, residenti nella directory packages/common/constructs
.
Implementare la tua API tRPC
Sezione intitolata “Implementare la tua API tRPC”A grandi linee, le API tRPC consistono in un router che delega le richieste a procedure specifiche. Ogni procedura ha un input e un output definiti come schemi Zod.
La directory src/schema
contiene i tipi condivisi tra codice client e server. In questo pacchetto, questi tipi sono definiti usando Zod, una libreria TypeScript-first per dichiarazione e validazione di schemi.
Uno schema di esempio potrebbe essere:
import { z } from 'zod';
// Definizione schemaexport const UserSchema = z.object({ name: z.string(), height: z.number(), dateOfBirth: z.string().datetime(),});
// Tipo TypeScript corrispondenteexport type User = z.TypeOf<typeof UserSchema>;
Dato lo schema sopra, il tipo User
equivale al seguente TypeScript:
interface User { name: string; height: number; dateOfBirth: string;}
Gli schemi sono condivisi da codice server e client, fornendo un unico punto di aggiornamento per modifiche alle strutture usate nella tua API.
Gli schemi sono automaticamente validati dalla tua API tRPC a runtime, evitando la necessità di scrivere logiche di validazione manuali nel backend.
Zod fornisce utility potenti per combinare o derivare schemi come .merge
, .pick
, .omit
e altri. Maggiori informazioni sul sito di documentazione Zod.
Router e Procedure
Sezione intitolata “Router e Procedure”Il punto d’ingresso della tua API si trova in src/router.ts
. Questo file contiene l’handler Lambda che instrada le richieste alle “procedure” in base all’operazione invocata. Ogni procedura definisce input atteso, output e implementazione.
Il router di esempio generato ha una singola operazione chiamata echo
:
import { echo } from './procedures/echo.js';
export const appRouter = router({ echo,});
La procedura echo
di esempio è generata in src/procedures/echo.ts
:
export const echo = publicProcedure .input(EchoInputSchema) .output(EchoOutputSchema) .query((opts) => ({ result: opts.input.message }));
Analizzando il codice:
publicProcedure
definisce un metodo pubblico nell’API, includendo il middleware configurato insrc/middleware
. Questo middleware include l’integrazione con AWS Lambda Powertools per logging, tracciamento e metriche.input
accetta uno schema Zod che definisce l’input atteso per l’operazione. Le richieste per questa operazione sono automaticamente validate rispetto a questo schema.output
accetta uno schema Zod che definisce l’output atteso. Vedrai errori di tipo nell’implementazione se non restituisci un output conforme.query
accetta una funzione che definisce l’implementazione. Questa riceveopts
, contenente l’input
passato, oltre al contesto configurato dal middleware disponibile inopts.ctx
. La funzione deve restituire un output conforme allo schemaoutput
.
L’uso di query
indica un’operazione non mutativa. Usalo per metodi di recupero dati. Per operazioni mutative, usa invece mutation
.
Se aggiungi una nuova procedura, assicurati di registrarla nel router in src/router.ts
.
Personalizzare la tua API tRPC
Sezione intitolata “Personalizzare la tua API tRPC”Nell’implementazione, puoi restituire errori ai client lanciando un TRPCError
. Questi accettano un code
che indica il tipo di errore, ad esempio:
throw new TRPCError({ code: 'NOT_FOUND', message: 'Impossibile trovare la risorsa richiesta',});
Organizzare le Operazioni
Sezione intitolata “Organizzare le Operazioni”Man mano che l’API cresce, potresti voler raggruppare operazioni correlate.
Puoi creare router annidati, ad 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 riceveranno questo raggruppamento, ad esempio invocare listUsers
apparirà così:
client.users.list.query();
Logging
Sezione intitolata “Logging”Il logger AWS Lambda Powertools è configurato in src/middleware/logger.ts
e accessibile nelle implementazioni via opts.ctx.logger
. Puoi usarlo per loggare su CloudWatch Logs e/o controllare valori aggiuntivi da includere in ogni messaggio di log strutturato. Esempio:
export const echo = publicProcedure .input(...) .output(...) .query(async (opts) => { opts.ctx.logger.info('Operazione chiamata con input', opts.input);
return ...; });
Per maggiori informazioni, consulta la documentazione AWS Lambda Powertools Logger.
Registrare Metriche
Sezione intitolata “Registrare Metriche”Le metriche AWS Lambda Powertools sono configurate in src/middleware/metrics.ts
e accessibili via opts.ctx.metrics
. Puoi usarle per registrare metriche in CloudWatch senza bisogno di AWS SDK, ad esempio:
export const echo = publicProcedure .input(...) .output(...) .query(async (opts) => { opts.ctx.metrics.addMetric('Invocations', 'Count', 1);
return ...; });
Per maggiori informazioni, consulta la documentazione AWS Lambda Powertools Metrics.
Ottimizzare il Tracciamento X-Ray
Sezione intitolata “Ottimizzare il Tracciamento X-Ray”Il tracer AWS Lambda Powertools è configurato in src/middleware/tracer.ts
e accessibile via opts.ctx.tracer
. Puoi usarlo per aggiungere tracce AWS X-Ray per monitorare performance e flusso delle richieste. Esempio:
export const echo = publicProcedure .input(...) .output(...) .query(async (opts) => { const subSegment = opts.ctx.tracer.getSegment()!.addNewSubsegment('MyAlgorithm'); // ... logica dell'algoritmo da tracciare subSegment.close();
return ...; });
Per maggiori informazioni, consulta la documentazione AWS Lambda Powertools Tracer.
Implementare Middleware Personalizzati
Sezione intitolata “Implementare Middleware Personalizzati”Puoi aggiungere valori al contesto delle procedure implementando middleware.
Ad esempio, implementiamo un middleware per estrarre dettagli sull’utente chiamante in src/middleware/identity.ts
.
Questo esempio assume auth
impostato a IAM
. Per autenticazione Cognito, il middleware è più diretto, estraendo i claim dall’event
.
Prima definiamo cosa aggiungere al contesto:
export interface IIdentityContext { identity?: { sub: string; username: string; };}
Nota che definiamo una proprietà opzionale. tRPC gestisce l’assicurazione che sia definita nelle procedure con middleware configurato correttamente.
Implementiamo il middleware:
export const createIdentityPlugin = () => { const t = initTRPC.context<...>().create(); return t.procedure.use(async (opts) => { // Logica pre-procedura
const response = await opts.next(...);
// Logica post-procedura
return response; });};
Nel nostro caso, estraiamo i dettagli dell’utente Cognito dall’evento API Gateway. L’implementazione varia tra API REST e HTTP:
import { CognitoIdentityProvider } from '@aws-sdk/client-cognito-identity-provider';import { initTRPC, TRPCError } from '@trpc/server';import { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda';import { APIGatewayProxyEvent } from 'aws-lambda';
export interface IIdentityContext { identity?: { sub: string; username: string; };}
export const createIdentityPlugin = () => { const t = initTRPC.context<IIdentityContext & CreateAWSLambdaContextOptions<APIGatewayProxyEvent>>().create();
const cognito = new CognitoIdentityProvider();
return t.procedure.use(async (opts) => { const cognitoAuthenticationProvider = opts.ctx.event.requestContext?.identity?.cognitoAuthenticationProvider;
let sub: string | undefined = undefined; if (cognitoAuthenticationProvider) { const providerParts = cognitoAuthenticationProvider.split(':'); sub = providerParts[providerParts.length - 1]; }
if (!sub) { throw new TRPCError({ code: 'FORBIDDEN', message: `Impossibile identificare l'utente chiamante`, }); }
const { Users } = await cognito.listUsers({ UserPoolId: process.env.USER_POOL_ID!, Limit: 1, Filter: `sub="${sub}"`, });
if (!Users || Users.length !== 1) { throw new TRPCError({ code: 'FORBIDDEN', message: `Nessun utente trovato con subjectId ${sub}`, }); }
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: `Impossibile identificare l'utente chiamante`, }); }
const { Users } = await cognito.listUsers({ UserPoolId: process.env.USER_POOL_ID!, Limit: 1, Filter: `sub="${sub}"`, });
if (!Users || Users.length !== 1) { throw new TRPCError({ code: 'FORBIDDEN', message: `Nessun utente trovato con subjectId ${sub}`, }); }
return await opts.next({ ctx: { ...opts.ctx, identity: { sub, username: Users[0].Username!, }, }, }); });};
Deploy dell’API tRPC
Sezione intitolata “Deploy dell’API tRPC”Il generatore crea un costrutto CDK per il deploy nella cartella common/constructs
. Puoi usarlo in un’applicazione CDK:
import { MyApi } from ':my-scope/common-constructs`;
export class ExampleStack extends Stack { constructor(scope: Construct, id: string) { const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), }); }}
Questo configura l’infrastruttura API, inclusi AWS API Gateway REST/HTTP API, funzioni Lambda e autenticazione in base al metodo auth
scelto.
Integrazioni Type-Safe
Sezione intitolata “Integrazioni Type-Safe”I costrutti CDK per le API REST/HTTP sono configurati per fornire un’interfaccia type-safe per definire le integrazioni per ciascuna delle tue operazioni.
Integrazioni predefinite
Sezione intitolata “Integrazioni predefinite”Puoi utilizzare il metodo statico defaultIntegrations
per sfruttare il pattern predefinito, che definisce una singola funzione AWS Lambda per ogni operazione:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
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 hai bisogno di aggiungere alcune autorizzazioni a questa funzione, puoi farlo come segue:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
// sayHello è tipizzato in base alle operazioni definite nella tua APIapi.integrations.sayHello.handler.addToRolePolicy(new PolicyStatement({ effect: Effect.ALLOW, actions: [...], resources: [...],}));
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 usare il metodo withDefaultOptions
. Ad esempio, se vuoi che tutte le tue funzioni Lambda risiedano in una Vpc:
const vpc = new Vpc(this, 'Vpc', ...);
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withDefaultOptions({ vpc, }) .build(),});
Override delle integrazioni
Sezione intitolata “Override delle integrazioni”Puoi anche sovrascrivere le integrazioni per operazioni specifiche usando il metodo withOverrides
. Ogni override deve specificare una proprietà integration
tipizzata secondo l’appropriato costrutto CDK di integrazione per l’API HTTP o REST. Il metodo withOverrides
è anch’esso type-safe. Ad esempio, se desideri sovrascrivere un’API getDocumentation
per puntare a documentazione ospitata su un sito esterno:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), }, }) .build(),});
Noterai che l’integrazione sovrascritta non avrà più una proprietà handler
quando vi accedi tramite api.integrations.getDocumentation
.
Puoi aggiungere proprietà aggiuntive a un’integrazione che saranno anch’esse tipizzate correttamente, permettendo l’astrazione di altri tipi di integrazione mantenendo il type-safety. Ad esempio, se hai creato un’integrazione S3 per un’API REST e successivamente vuoi riferirti al bucket per una specifica operazione:
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(),});
// Successivamente, magari in un altro file, puoi accedere alla proprietà bucket che abbiamo definito// in modo type-safeapi.integrations.getFile.bucket.grantRead(...);
Override degli autorizzatori
Sezione intitolata “Override degli autorizzatori”Puoi anche fornire options
nelle tue integrazioni per sovrascrivere opzioni specifiche dei metodi come gli autorizzatori. Ad esempio, se desideri usare l’autenticazione Cognito per l’operazione getDocumentation
:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), options: { authorizer: new CognitoUserPoolsAuthorizer(...) // per REST, o HttpUserPoolAuthorizer per un'API HTTP } }, }) .build(),});
Integrazioni esplicite
Sezione intitolata “Integrazioni esplicite”Se preferisci, puoi scegliere di non usare le integrazioni predefinite e fornirne direttamente una per ogni operazione. Questo è utile se, ad esempio, ogni operazione necessita di un tipo diverso di integrazione o vuoi ricevere un errore di tipo quando aggiungi nuove operazioni:
new MyApi(this, 'MyApi', { integrations: { sayHello: { integration: new LambdaIntegration(...), }, getDocumentation: { integration: new HttpIntegration(...), }, },});
Pattern Router
Sezione intitolata “Pattern Router”Se preferisci distribuire una singola funzione Lambda per gestire tutte le richieste API, puoi modificare liberamente il metodo defaultIntegrations
della tua API per creare una singola funzione invece di una per integrazione:
export class MyApi<...> extends ... {
public static defaultIntegrations = (scope: Construct) => { const router = new Function(scope, 'RouterHandler', { ... }); return IntegrationBuilder.rest({ ... defaultIntegrationOptions: {}, buildDefaultIntegration: (op) => { return { // Riferimento allo stesso router lambda handler in ogni integrazione integration: new LambdaIntegration(router), }; }, }); };}
Puoi modificare il codice in altri modi se preferisci, ad esempio potresti definire la funzione router
come parametro di defaultIntegrations
invece di costruirla all’interno del metodo.
Concessione Accesso (Solo IAM)
Sezione intitolata “Concessione Accesso (Solo IAM)”Con autenticazione IAM
, puoi usare grantInvokeAccess
per concedere accesso all’API, ad esempio agli utenti Cognito autenticati:
api.grantInvokeAccess(myIdentityPool.authenticatedRole);
Server tRPC Locale
Sezione intitolata “Server tRPC Locale”Puoi usare il target serve
per eseguire un server locale:
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
Il punto d’ingresso è src/local-server.ts
. Il server si ricarica automaticamente alle modifiche.
Invocare l’API tRPC
Sezione intitolata “Invocare l’API tRPC”Puoi creare un client tRPC type-safe per invocare l’API. Per chiamate da altri backend:
import { createMyApiClient } from ':my-scope/my-api';
const client = createMyApiClient({ url: 'https://my-api-url.example.com/' });
await client.echo.query({ message: 'Hello world!' });
Per chiamate da un sito React, considera il generatore API Connection.
Ulteriori Informazioni
Sezione intitolata “Ulteriori Informazioni”Per maggiori dettagli su tRPC, consulta la documentazione tRPC.