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 - The name of the API (required). Used to generate class names and file paths.
    computeType string ServerlessApiGatewayRestApi The type of compute to use to deploy this API. Choose between ServerlessApiGatewayRestApi (default) or ServerlessApiGatewayHttpApi.
    auth string IAM The method used to authenticate with your API. Choose between IAM (default), Cognito or None.
    directory string packages The directory to store the application in.
    iacProvider string Inherit The preferred IaC provider. By default this is inherited from your initial selection.

    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 schema 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 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.

    O ponto de entrada da API está em src/router.ts. Este arquivo contém o handler Lambda que roteia requisições para “procedimentos” baseado 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 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.

    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.

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

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

    Exemplo completo para REST e HTTP APIs:

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

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

    Se você preferir implantar uma única função Lambda para atender todas as requisições da API, pode livremente editar o método defaultIntegrations de sua API para criar 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) => {
    const router = new Function(scope, 'RouterHandler', { ... });
    return IntegrationBuilder.rest({
    ...
    defaultIntegrationOptions: {},
    buildDefaultIntegration: (op) => {
    return {
    // Referencia o mesmo router lambda handler em todas as integrações
    integration: new LambdaIntegration(router),
    };
    },
    });
    };
    }

    Você pode modificar o código de outras formas se preferir, por exemplo pode definir a função router como parâmetro para defaultIntegrations em vez de construí-la dentro do método.

    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 run <project-name>:bundle

    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 run @my-scope/my-api:serve

    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 API Connection.

    Consulte a documentação do tRPC.