Pular para o conteúdo

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 imediatamente refletidas no código do cliente e visíveis na sua IDE sem necessidade de reconstruir o projeto.

O gerador de API tRPC cria uma nova API tRPC com configuração de infraestrutura AWS CDK ou Terraform. O backend gerado utiliza AWS Lambda para implantação serverless, exposto via AWS API Gateway API, e inclui validação de schema usando Zod. Configura AWS Lambda Powertools para observabilidade, incluindo logging, rastreamento com AWS X-Ray e métricas no Cloudwatch.

Você pode gerar uma nova API tRPC de duas formas:

  1. Instale o Nx Console VSCode Plugin se ainda não o fez
  2. Abra o console Nx no VSCode
  3. Clique em Generate (UI) na seção "Common Nx Commands"
  4. Procure por @aws/nx-plugin - ts#trpc-api
  5. Preencha os parâmetros obrigatórios
    • Clique em Generate
    Parâmetro Tipo Padrão Descrição
    name Obrigatório string - O nome da API (obrigatório). Usado para gerar nomes de classes e caminhos de arquivos.
    computeType string ServerlessApiGatewayRestApi O tipo de computação a ser usado para implantar esta API. Escolha entre ServerlessApiGatewayRestApi (padrão) ou ServerlessApiGatewayHttpApi.
    integrationPattern string isolated Como as integrações do API Gateway são geradas para a API. Escolha entre isolated (padrão) e shared.
    auth string IAM O método usado para autenticar com sua API. Escolha entre IAM (padrão), Cognito ou None.
    directory string packages O diretório para armazenar a aplicação.
    subDirectory string - O subdiretório onde o projeto é colocado. Por padrão, este é o nome do projeto.
    iacProvider string Inherit O provedor de IaC preferido. Por padrão, isso é herdado da sua seleção inicial.

    O gerador criará a seguinte estrutura de projeto no diretório <directory>/<api-name>:

    • Directorysrc
      • init.ts Inicialização do backend tRPC
      • handler.ts Ponto de entrada do handler Lambda
      • router.ts Definição do roteador tRPC
      • Directoryschema Definições de schema usando Zod
        • echo.ts Exemplo de definições para entrada e saída do procedimento “echo”
        • z-async-iterable.ts Helper Zod para subscriptions (somente REST API)
      • 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 no Lambda
        • tracer.ts middleware para configurar AWS Powertools para rastreamento no Lambda
        • metrics.ts middleware para configurar AWS Powertools para métricas no 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

    Como este gerador fornece infraestrutura como código com base no iacProvider escolhido, ele criará um projeto em packages/common que inclui os constructs CDK ou módulos Terraform relevantes.

    O projeto comum de infraestrutura como código está estruturado da seguinte forma:

    • Directorypackages/common/constructs
      • Directorysrc
        • Directoryapp/ Constructs para infraestrutura específica de um projeto/gerador
        • Directorycore/ Constructs genéricos reutilizados pelos constructs em app
        • index.ts Ponto de entrada exportando os constructs de app
      • project.json Metas de build e configuração do projeto

    Para implantar sua API, os seguintes arquivos são gerados:

    • Directorypackages/common/constructs/src
      • Directoryapp
        • Directoryapis
          • <project-name>.ts Construto CDK para implantar sua API
      • Directorycore
        • Directoryapi
          • http-api.ts Construto CDK para implantar uma API HTTP (se você escolheu implantar uma API HTTP)
          • rest-api.ts Construto CDK para implantar uma API REST (se você escolheu implantar uma API REST)
          • utils.ts Utilitários para os construtos da API

    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 schemas Zod.

    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 de declaração e validação de schemas TypeScript-first.

    Um exemplo de schema pode ser:

    import { z } from 'zod';
    // Definição do schema
    export const UserSchema = z.object({
    name: z.string(),
    height: z.number(),
    dateOfBirth: z.string().datetime(),
    });
    // Tipo TypeScript correspondente
    export type User = z.TypeOf<typeof UserSchema>;

    Dado o schema acima, o tipo User é equivalente ao seguinte TypeScript:

    interface User {
    name: string;
    height: number;
    dateOfBirth: string;
    }

    Schemas são compartilhados entre código servidor e cliente, provendo um único local para atualizações quando alterar estruturas usadas na sua API.

    Schemas são automaticamente validados 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 schemas como .merge, .pick, .omit e mais. Mais informações no site de documentação do Zod.

    Seu roteador tRPC é definido em src/router.ts, que registra todos os procedimentos. Cada procedimento define a entrada esperada, saída e implementação. O ponto de entrada do handler Lambda está em src/handler.ts, que encaminha requisições para seu roteador.

    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 em src/middleware. Este middleware inclui integração com AWS Lambda Powertools para logging, rastreamento e métricas.
    • input aceita um schema Zod que define a entrada esperada para a operação. Requisições são automaticamente validadas contra este schema.
    • output aceita um schema Zod que define a saída esperada. Erros de tipo serão exibidos se a implementação não retornar uma saída conforme o schema.
    • query aceita uma função que define a implementação. Recebe opts, que contém o input passado para a operação, além de contexto configurado por middleware disponível em opts.ctx. A função deve retornar uma saída conforme o schema output.

    O uso de query indica que a operação não é mutativa. Use para métodos de recuperação de dados. Para operações mutativas, use mutation.

    Ao adicionar novos procedimentos, registre-os no roteador em src/router.ts.

    tRPC subscriptions permitem transmitir dados do servidor para o cliente usando Server-Sent Events (SSE). Quando você seleciona ServerlessApiGatewayRestApi como seu tipo de computação, o gerador configura automaticamente a infraestrutura necessária para streaming, bem como um handler Lambda de streaming e o helper de schema ZodAsyncIterable.

    Para definir um procedimento de subscription, use o método .subscription com uma função geradora assíncrona. Use o helper ZodAsyncIterable de src/schema/z-async-iterable.ts para definir o schema de saída:

    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) {
    // Retorna dados ao cliente conforme ficam disponíveis
    for (const chunk of await getResults(opts.input.query)) {
    yield { text: chunk };
    }
    });

    Registre a subscription no seu roteador como qualquer outro procedimento:

    export const appRouter = router({
    echo,
    myStream,
    });

    A infraestrutura gerada usa um handler Lambda de streaming com ResponseTransferMode.STREAM no API Gateway para todas as operações REST API, o que permite que subscriptions funcionem junto com queries e mutations regulares.

    Na implementação, você pode retornar erros lançando TRPCError. Estes aceitam um code indicando o tipo de erro, por exemplo:

    throw new TRPCError({
    code: 'NOT_FOUND',
    message: 'O recurso solicitado não foi encontrado',
    });

    Conforme a API cresce, você pode agrupar operações relacionadas.

    Agrupe operações usando 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 recebem este agrupamento, por exemplo invocando listUsers:

    client.users.list.query();

    O logger AWS Lambda Powertools é configurado em src/middleware/logger.ts e acessado via opts.ctx.logger. Use para registrar logs no CloudWatch e/ou controlar valores adicionais em cada mensagem de log. Exemplo:

    export const echo = publicProcedure
    .input(...)
    .output(...)
    .query(async (opts) => {
    opts.ctx.logger.info('Operação chamada com input', opts.input);
    return ...;
    });

    Para mais informações, consulte a documentação do AWS Lambda Powertools Logger.

    Métricas do AWS Lambda Powertools são configuradas em src/middleware/metrics.ts e acessadas via opts.ctx.metrics. Use para registrar métricas no CloudWatch sem necessidade do AWS SDK:

    export const echo = publicProcedure
    .input(...)
    .output(...)
    .query(async (opts) => {
    opts.ctx.metrics.addMetric('Invocations', 'Count', 1);
    return ...;
    });

    Para mais informações, consulte a documentação do AWS Lambda Powertools Metrics.

    O tracer AWS Lambda Powertools é configurado em src/middleware/tracer.ts e acessado via opts.ctx.tracer. Use para adicionar traces com AWS X-Ray:

    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 ...;
    });

    Para mais informações, consulte a documentação do AWS Lambda Powertools Tracer.

    Adicione valores ao contexto de procedimentos implementando middleware.

    Exemplo: middleware para extrair detalhes do usuário em src/middleware/identity.ts.

    Premissa de autenticação

    Este exemplo assume auth como IAM. Para autenticação Cognito, o middleware é mais direto, extraindo claims do event.

    Primeiro definimos o contexto adicional:

    export interface IIdentityContext {
    identity?: {
    sub: string;
    username: string;
    };
    }

    Note que definimos uma propriedade adicional opcional no contexto. tRPC gerencia a garantia de que isso esteja definido em procedimentos que configuraram corretamente este middleware.

    Em seguida, implementamos o middleware:

    export const createIdentityPlugin = () => {
    const t = initTRPC.context<...>().create();
    return t.procedure.use(async (opts) => {
    // Lógica antes do procedimento
    const response = await opts.next(...);
    // Lógica após o procedimento
    return response;
    });
    };

    No nosso caso, queremos extrair detalhes sobre o usuário Cognito chamador. Faremos isso extraindo o ID de assunto do usuário (ou “sub”) do evento do API Gateway e recuperando detalhes do usuário do Cognito. A implementação varia ligeiramente dependendo se o evento foi fornecido à nossa função por uma REST API ou uma 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: `Não foi possível determinar o usuário chamador`,
    });
    }
    const { Users } = await cognito.listUsers({
    // Assume que o ID do user pool está configurado no ambiente Lambda
    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}`,
    });
    }
    // Fornece a identidade para outros procedimentos no contexto
    return await opts.next({
    ctx: {
    ...opts.ctx,
    identity: {
    sub,
    username: Users[0].Username!,
    },
    },
    });
    });
    };

    O gerador cria infraestrutura como código CDK ou Terraform baseado no iacProvider selecionado. Use para implantar sua API.

    O construct CDK para implantação está 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.

    Os construtos CDK da API REST/HTTP são configurados para fornecer uma interface type-safe para definir integrações para cada uma de suas operações.

    Os construtos CDK fornecem suporte completo a integrações type-safe conforme descrito abaixo.

    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(),
    });

    Você pode acessar as funções AWS Lambda subjacentes através da propriedade integrations do construto da API, de forma type-safe. Por exemplo, se sua API define uma operação chamada sayHello e você precisa adicionar 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 conforme as operações definidas em sua API
    api.integrations.sayHello.handler.addToRolePolicy(new PolicyStatement({
    effect: Effect.ALLOW,
    actions: [...],
    resources: [...],
    }));

    Se sua API usa o padrão shared, o roteador Lambda compartilhado é exposto como api.integrations.$router:

    const api = new MyApi(this, 'MyApi', {
    integrations: MyApi.defaultIntegrations(this).build(),
    });
    api.integrations.$router.handler.addEnvironment('LOG_LEVEL', 'DEBUG');

    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(),
    });

    Você também pode sobrescrever integrações para operações específicas usando o método withOverrides. Cada sobrescrita deve especificar uma propriedade integration que é tipada ao construto de integração CDK apropriado para a API HTTP ou REST. O método withOverrides também é type-safe. Por exemplo, se você deseja sobrescrever 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 sobrescrita não terá mais uma propriedade handler quando acessada via api.integrations.getDocumentation.

    Você pode adicionar propriedades adicionais a uma integração que também serão tipadas adequadamente, permitindo que outros tipos de integração sejam abstraídos mas permaneçam type-safe. Por exemplo, se você criou uma integração S3 para uma API REST e depois 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 forma type-safe
    api.integrations.getFile.bucket.grantRead(...);

    Você também pode fornecer options em sua integração para sobrescrever opções específicas de método como autorizadores. Por exemplo, se desejar usar autenticação Cognito para sua 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(),
    });

    Se preferir, você pode optar por não usar as integrações padrão e fornecer diretamente uma para cada operação. Isso é útil se, por exemplo, cada operação precisar usar um tipo diferente de integração ou se você quiser receber um erro de tipo ao adicionar novas operações:

    new MyApi(this, 'MyApi', {
    integrations: {
    sayHello: {
    integration: new LambdaIntegration(...),
    },
    getDocumentation: {
    integration: new HttpIntegration(...),
    },
    },
    });

    Os construtos CDK de API gerados suportam dois padrões de integração:

    • isolated cria uma função Lambda por operação. Este é o padrão para APIs geradas.
    • shared cria um único roteador Lambda padrão e o reutiliza para cada operação, a menos que você sobrescreva integrações específicas.

    isolated oferece permissões e configuração mais granulares por operação. shared reduz a proliferação de funções Lambda e integrações do API Gateway, ainda permitindo sobrescritas seletivas.

    Por exemplo, definir pattern como 'shared' cria uma única função em vez de uma por integração:

    packages/common/constructs/src/app/apis/my-api.ts
    export class MyApi<...> extends ... {
    public static defaultIntegrations = (scope: Construct) => {
    ...
    return IntegrationBuilder.rest({
    pattern: 'shared',
    ...
    });
    };
    }

    Para autenticação IAM:

    api.grantInvokeAccess(myIdentityPool.authenticatedRole);

    O gerador configura automaticamente um destino bundle que utiliza o Rolldown para criar um pacote de implantação:

    Terminal window
    pnpm nx bundle <project-name>

    A configuração do Rolldown pode ser encontrada em rolldown.config.ts, com uma entrada para cada pacote a ser gerado. O Rolldown gerencia a criação de múltiplos pacotes em paralelo, se definidos.

    Execute localmente com:

    Terminal window
    pnpm nx serve my-api

    O ponto de entrada é src/local-server.ts. Recarrega automaticamente em alterações.

    Crie um cliente type-safe para invocar a API:

    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 React, use o gerador Connection.

    Consulte a documentação do tRPC.