tRPC
tRPC é um framework para construir APIs em TypeScript com segurança de tipos end-to-end. Usando tRPC, atualizações nas entradas e saídas das operações da API são refletidas imediatamente no código do cliente e visíveis em sua IDE sem necessidade de reconstruir o projeto.
O gerador de API tRPC cria uma nova API tRPC com infraestrutura configurada usando AWS CDK. O backend gerado utiliza AWS Lambda para implantação serverless e inclui validação de esquema com Zod. Configura AWS Lambda Powertools para observabilidade, incluindo logging, rastreamento com AWS X-Ray e métricas no CloudWatch.
Utilização
Seção intitulada “Utilização”Gerar uma API tRPC
Seção intitulada “Gerar uma API tRPC”Você pode gerar uma nova API tRPC de duas formas:
- Instale o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)
na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - ts#trpc-api
- Preencha os parâmetros obrigatórios
- Clique em
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
Você também pode realizar uma execução simulada para ver quais arquivos seriam alterados
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
Parâmetro | Tipo | Padrão | Descrição |
---|---|---|---|
name Obrigatório | 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. |
Saída do Gerador
Seção intitulada “Saída do Gerador”O gerador criará a seguinte estrutura de projeto no diretório <directory>/<api-name>
:
Directorysrc
- init.ts Inicialização do backend tRPC
- router.ts Definição do roteador tRPC (ponto de entrada da API no handler Lambda)
Directoryschema Definições de esquema usando Zod
- echo.ts Exemplo de definições para entrada e saída do procedimento “echo”
Directoryprocedures Procedimentos (ou operações) expostos pela sua API
- echo.ts Procedimento de exemplo
Directorymiddleware
- error.ts Middleware para tratamento de erros
- logger.ts middleware para configurar AWS Powertools para logging em Lambda
- tracer.ts middleware para configurar AWS Powertools para rastreamento em Lambda
- metrics.ts middleware para configurar AWS Powertools para métricas em Lambda
- local-server.ts Ponto de entrada do adaptador standalone tRPC para servidor de desenvolvimento local
Directoryclient
- index.ts Cliente type-safe para chamadas máquina-a-máquina na API
- tsconfig.json Configuração TypeScript
- project.json Configuração do projeto e targets de build
O gerador também criará constructs CDK que podem ser usados para implantar sua API, residindo no diretório packages/common/constructs
.
Implementando sua API tRPC
Seção intitulada “Implementando sua API tRPC”Em alto nível, APIs tRPC consistem em um roteador que delega requisições para procedimentos específicos. Cada procedimento possui uma entrada e saída definidas como um esquema Zod.
Esquema
Seção intitulada “Esquema”O diretório src/schema
contém os tipos compartilhados entre seu código cliente e servidor. Neste pacote, esses tipos são definidos usando Zod, uma biblioteca TypeScript-first para declaração e validação de esquemas.
Um exemplo de esquema pode ser:
import { z } from 'zod';
// Definição do esquemaexport const UserSchema = z.object({ name: z.string(), height: z.number(), dateOfBirth: z.string().datetime(),});
// Tipo TypeScript correspondenteexport type User = z.TypeOf<typeof UserSchema>;
Dado o esquema acima, o tipo User
é equivalente ao seguinte TypeScript:
interface User { name: string; height: number; dateOfBirth: string;}
Esquemas são compartilhados por código cliente e servidor, proporcionando um único local para atualizações nas estruturas usadas na API.
Esquemas são validados automaticamente pela sua API tRPC em runtime, eliminando a necessidade de criar lógica de validação manual no backend.
Zod fornece utilitários poderosos para combinar ou derivar esquemas como .merge
, .pick
, .omit
e mais. Mais informações no site de documentação do Zod.
Roteador e Procedimentos
Seção intitulada “Roteador e Procedimentos”O ponto de entrada da API está em src/router.ts
. Este arquivo contém o handler Lambda que roteia requisições para “procedimentos” baseados na operação invocada. Cada procedimento define a entrada esperada, saída e implementação.
O roteador de exemplo gerado possui uma única operação chamada echo
:
import { echo } from './procedures/echo.js';
export const appRouter = router({ echo,});
O procedimento echo
de exemplo é gerado em src/procedures/echo.ts
:
export const echo = publicProcedure .input(EchoInputSchema) .output(EchoOutputSchema) .query((opts) => ({ result: opts.input.message }));
Analisando o código acima:
publicProcedure
define um método público na API, incluindo middleware configurado emsrc/middleware
. Este middleware inclui integração com AWS Lambda Powertools para logging, rastreamento e métricas.input
aceita um esquema Zod que define a entrada esperada para a operação. Requisições são validadas automaticamente contra este esquema.output
aceita um esquema Zod que define a saída esperada. Erros de tipo aparecerão na implementação se não retornar uma saída conforme o esquema.query
aceita uma função que define a implementação da operação. Recebeopts
contendo oinput
passado e contexto configurado por middleware disponível emopts.ctx
. A função deve retornar uma saída conforme o esquemaoutput
.
O uso de query
indica uma operação não mutativa. Use para recuperar dados. Para operações mutativas, use mutation
.
Ao adicionar novo procedimento, registre-o no roteador em src/router.ts
.
Personalizando sua API tRPC
Seção intitulada “Personalizando sua API tRPC”Para retornar erros, lance TRPCError
com um code
indicando o tipo de erro:
throw new TRPCError({ code: 'NOT_FOUND', message: 'O recurso solicitado não foi encontrado',});
Organizando Operações
Seção intitulada “Organizando Operações”Para agrupar operações relacionadas, use roteadores aninhados:
import { getUser } from './procedures/users/get.js';import { listUsers } from './procedures/users/list.js';
const appRouter = router({ users: router({ get: getUser, list: listUsers, }), ...})
Clientes verão o agrupamento, por exemplo:
client.users.list.query();
Logging
Seção intitulada “Logging”O logger AWS Lambda Powertools é configurado em src/middleware/logger.ts
e acessível via opts.ctx.logger
. Use para registrar logs no CloudWatch:
export const echo = publicProcedure .input(...) .output(...) .query(async (opts) => { opts.ctx.logger.info('Operação chamada com input', opts.input);
return ...; });
Mais informações na documentação do AWS Lambda Powertools Logger.
Registrando Métricas
Seção intitulada “Registrando Métricas”Métricas do AWS Lambda Powertools são configuradas em src/middleware/metrics.ts
e acessíveis via opts.ctx.metrics
:
export const echo = publicProcedure .input(...) .output(...) .query(async (opts) => { opts.ctx.metrics.addMetric('Invocations', 'Count', 1);
return ...; });
Mais informações na documentação de Métricas.
Ajustando Rastreamento X-Ray
Seção intitulada “Ajustando Rastreamento X-Ray”O tracer AWS Lambda Powertools é configurado em src/middleware/tracer.ts
e acessível via opts.ctx.tracer
:
export const echo = publicProcedure .input(...) .output(...) .query(async (opts) => { const subSegment = opts.ctx.tracer.getSegment()!.addNewSubsegment('MyAlgorithm'); // ... lógica do algoritmo para capturar subSegment.close();
return ...; });
Mais informações na documentação do Tracer.
Implementando Middleware Customizado
Seção intitulada “Implementando Middleware Customizado”Para adicionar valores ao contexto dos procedimentos, implemente middleware.
Exemplo de middleware para extrair identidade do usuário em src/middleware/identity.ts
:
Este exemplo assume auth
como IAM
. Para autenticação Cognito, o middleware é mais direto, extraindo claims do event
.
Defina o contexto adicional:
export interface IIdentityContext { identity?: { sub: string; username: string; };}
Implemente o middleware:
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: `Não foi possível determinar o usuário chamador`, }); }
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: `Nenhum usuário encontrado com 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: `Não foi possível determinar o usuário chamador`, }); }
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: `Nenhum usuário encontrado com subjectId ${sub}`, }); }
return await opts.next({ ctx: { ...opts.ctx, identity: { sub, username: Users[0].Username!, }, }, }); });};
Implantando sua API tRPC
Seção intitulada “Implantando sua API tRPC”O gerador cria um construct CDK em common/constructs
. Exemplo de uso:
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(), }); }}
Isso configura infraestrutura incluindo AWS API Gateway, AWS Lambda e autenticação baseada no método auth
escolhido.
Integrações Type-Safe
Seção intitulada “Integrações Type-Safe”Os construtores CDK da API REST/HTTP são configurados para fornecer uma interface tipada para definir integrações para cada uma de suas operações.
Integrações Padrão
Seção intitulada “Integrações Padrão”Você pode usar o método estático defaultIntegrations
para utilizar o padrão padrão, que define uma função AWS Lambda individual para cada operação:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
Acessando Integrações
Seção intitulada “Acessando Integrações”Você pode acessar as funções AWS Lambda subjacentes através da propriedade integrations
do construto da API, de maneira tipada. Por exemplo, se sua API define uma operação chamada sayHello
e você precisa adicionar algumas permissões a esta função, você pode fazer isso da seguinte forma:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
// sayHello é tipado para as operações definidas em sua APIapi.integrations.sayHello.handler.addToRolePolicy(new PolicyStatement({ effect: Effect.ALLOW, actions: [...], resources: [...],}));
Personalizando Opções Padrão
Seção intitulada “Personalizando Opções Padrão”Se você deseja personalizar as opções usadas ao criar a função Lambda para cada integração padrão, pode usar o método withDefaultOptions
. Por exemplo, se deseja que todas suas funções Lambda residam em uma Vpc:
const vpc = new Vpc(this, 'Vpc', ...);
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withDefaultOptions({ vpc, }) .build(),});
Substituindo Integrações
Seção intitulada “Substituindo Integrações”Você também pode substituir integrações para operações específicas usando o método withOverrides
. Cada substituição deve especificar uma propriedade integration
tipada para o construto de integração CDK apropriado para a API HTTP ou REST. O método withOverrides
também é tipado. Por exemplo, se você deseja substituir uma API getDocumentation
para apontar para documentação hospedada em um site externo:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), }, }) .build(),});
Você notará que a integração substituída não terá mais uma propriedade handler
quando acessada via api.integrations.getDocumentation
.
Você pode adicionar propriedades extras a uma integração que também serão tipadas adequadamente, permitindo que outros tipos de integração sejam abstraídos mas mantenham a tipagem. Por exemplo, se você criou uma integração S3 para uma API REST e posteriormente deseja referenciar o bucket para uma operação específica:
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(),});
// Posteriormente, talvez em outro arquivo, você pode acessar a propriedade bucket// que definimos de maneira tipadaapi.integrations.getFile.bucket.grantRead(...);
Substituindo Autorizadores
Seção intitulada “Substituindo Autorizadores”Você também pode fornecer options
em sua integração para substituir opções específicas de método, como autorizadores. Por exemplo, para usar autenticação Cognito na operação getDocumentation
:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), options: { authorizer: new CognitoUserPoolsAuthorizer(...) // para REST, ou HttpUserPoolAuthorizer para HTTP API } }, }) .build(),});
Integrações Explícitas
Seção intitulada “Integrações Explícitas”Se preferir, você pode optar por não usar as integrações padrão e fornecer uma integração explicitamente para cada operação. Isso é útil se, por exemplo, cada operação precisar usar um tipo diferente de integração:
new MyApi(this, 'MyApi', { integrations: { sayHello: { integration: new LambdaIntegration(...), }, getDocumentation: { integration: new HttpIntegration(...), }, },});
Padrão de Roteador
Seção intitulada “Padrão de Roteador”Se preferir implantar uma única função Lambda para atender todas as requisições da API, você pode editar livremente o método defaultIntegrations
para criar uma única função em vez de uma por integração:
export class MyApi<...> extends ... {
public static defaultIntegrations = (scope: Construct) => { const router = new Function(scope, 'RouterHandler', { ... }); return IntegrationBuilder.rest({ ... defaultIntegrationOptions: {}, buildDefaultIntegration: (op) => { return { // Referencia o mesmo handler Lambda do roteador em todas as integrações integration: new LambdaIntegration(router), }; }, }); };}
Você pode modificar o código de outras formas se preferir, como definir a função router
como parâmetro do defaultIntegrations
em vez de construí-la dentro do método.
Concedendo Acesso (Somente IAM)
Seção intitulada “Concedendo Acesso (Somente IAM)”Para autenticação IAM
, use grantInvokeAccess
para conceder acesso:
api.grantInvokeAccess(myIdentityPool.authenticatedRole);
Servidor tRPC Local
Seção intitulada “Servidor tRPC Local”Use o target serve
para executar localmente:
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
O ponto de entrada é src/local-server.ts
. Recarrega automaticamente ao fazer alterações.
Invocando sua API tRPC
Seção intitulada “Invocando sua API tRPC”Crie um cliente type-safe usando src/client/index.ts
:
import { createMyApiClient } from ':my-scope/my-api';
const client = createMyApiClient({ url: 'https://my-api-url.example.com/' });
await client.echo.query({ message: 'Hello world!' });
Para sites React, use o gerador API Connection.
Mais Informações
Seção intitulada “Mais Informações”Consulte a documentação do tRPC para mais detalhes.