Usar tRPC no Node.js permite criar APIs totalmente tipadas entre servidor e cliente TypeScript sem manter um schema separado ou executar uma etapa de geração de código. O servidor define procedures, entradas e saídas, e o cliente recebe inferência automática para chamadas, erros e respostas.
O ganho de produtividade é especialmente forte em aplicações full-stack que compartilham o mesmo repositório ou pacote de tipos. Porém, type safety em tempo de compilação não substitui validação runtime, autenticação, autorização, limites, versionamento nem observabilidade. Dados enviados pela rede continuam não confiáveis.
Neste guia, você aprenderá a configurar tRPC 11, criar routers e procedures, validar entradas, construir context, proteger rotas, integrar com HTTP, aplicar batching, tratar erros, usar subscriptions e testar a API.
O que é tRPC?
tRPC é uma biblioteca para APIs end-to-end typesafe. A documentação oficial do tRPC explica que a versão 11 oferece inferência de tipos sem geração de código e possui adapters para Node.js e frameworks populares. A documentação do TypeScript apresenta os recursos de tipos usados pela biblioteca.
Quando usar?
- frontend e backend em TypeScript;
- monorepo ou packages compartilhados;
- painéis internos;
- aplicações web full-stack;
- BFFs controlados pela mesma equipe;
- migração gradual de endpoints.
Quando evitar?
- API pública para linguagens variadas;
- contrato que precisa de OpenAPI independente;
- clientes externos sem TypeScript;
- equipes que precisam desacoplar releases completamente;
- protocolos com governança formal entre empresas.
Para contratos públicos, consulte OpenAPI com Node.js.
Instalando
npm install @trpc/server @trpc/client zodZod é uma opção comum para validar entradas. A API exata deve acompanhar a versão instalada.
Inicializando tRPC
import { initTRPC } from '@trpc/server';
export type Context = {
requestId: string;
auth: AuthContext | null;
services: Services;
};
const t = initTRPC.context<Context>().create();
export const router = t.router;
export const publicProcedure = t.procedure;O context é criado por requisição e pode carregar identidade, serviços, repositories e request ID.
Router básico
import { z } from 'zod';
export const appRouter = router({
health: publicProcedure.query(() => ({
status: 'ok'
})),
userById: publicProcedure
.input(z.object({
id: z.string().uuid()
}))
.query(({ input, ctx }) => {
return ctx.services.users.findById(input.id);
})
});
export type AppRouter = typeof appRouter;O tipo AppRouter é exportado para o cliente sem enviar código do servidor ao navegador.
Cliente tipado
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from './server/router';
const client = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: 'https://app.example.com/trpc'
})
]
});
const user = await client.userById.query({
id: userId
});O editor conhece entrada e resposta. Mudanças incompatíveis aparecem durante build dos clientes que compartilham o tipo.
Type safety não valida a rede
Um cliente malicioso pode enviar qualquer JSON. Por isso, toda procedure que recebe input precisa de schema runtime:
.input(z.object({
name: z.string().min(1).max(100),
email: z.string().email()
}).strict())O artigo sobre validação com Zod será incluído nesta mesma rodada.
Queries e mutations
- query: leitura sem efeito relevante;
- mutation: criação, alteração ou exclusão;
- subscription: atualizações em tempo real.
createUser: protectedProcedure
.input(CreateUserSchema)
.mutation(({ input, ctx }) => {
return ctx.services.users.create({
tenantId: ctx.auth.tenantId,
actorId: ctx.auth.userId,
input
});
})Context por requisição
export async function createContext({ req }) {
const requestId = resolveRequestId(req);
const auth = await authenticate(req);
return {
requestId,
auth,
services
};
}Nunca guarde usuário ou tenant em variável global.
Middleware de autenticação
import { TRPCError } from '@trpc/server';
const requireAuth = t.middleware(({ ctx, next }) => {
if (!ctx.auth) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Autenticação necessária'
});
}
return next({
ctx: {
...ctx,
auth: ctx.auth
}
});
});
export const protectedProcedure = t.procedure.use(requireAuth);Autorização
function requirePermission(permission) {
return t.middleware(({ ctx, next }) => {
if (!ctx.auth?.permissions.includes(permission)) {
throw new TRPCError({ code: 'FORBIDDEN' });
}
return next();
});
}Consulte RBAC no Node.js e ABAC no Node.js.
Autorização por recurso
Uma permission geral não basta. A consulta deve incluir tenant e ownership:
const project = await ctx.services.projects.findOne({
tenantId: ctx.auth.tenantId,
projectId: input.id
});Não busque apenas por ID e valide depois quando o banco pode aplicar o escopo desde o início.
Multi-tenancy
O tenant vem da identidade autenticada, nunca de um campo livre no input. Consulte Multi-Tenancy no Node.js.
Adapters HTTP
tRPC pode rodar com servidor HTTP nativo, Express, Fastify, Next.js e outros adapters. Escolha a integração que oferece controle de cookies, CORS e shutdown para sua aplicação.
Servidor HTTP standalone
import { createHTTPServer } from '@trpc/server/adapters/standalone';
const server = createHTTPServer({
router: appRouter,
createContext
});
server.listen(3000);Integração com Express
app.use('/trpc', createExpressMiddleware({
router: appRouter,
createContext
}));Aplique body limits, CORS e middlewares antes da integração conforme necessário.
Request batching
O httpBatchLink pode combinar chamadas próximas em uma única requisição HTTP. Isso reduz overhead de rede.
Limites de batching
Defina quantidade e tamanho máximos. Um cliente não deve agrupar centenas de procedures caras em um único request para contornar rate limiting.
Rate limiting
Limite por procedure, usuário, tenant e custo. Consulte Rate Limiting no Node.js.
Complexidade
tRPC não possui queries arbitrariamente profundas como GraphQL, mas uma procedure ainda pode receber listas enormes, filtros caros e múltiplos itens em batch.
Paginação
.input(z.object({
cursor: z.string().optional(),
limit: z.number().int().min(1).max(100).default(20)
}))Consulte Paginação em APIs Node.js.
Transformação de dados
JSON não preserva Date, Map, BigInt e outros tipos automaticamente. Use serializer suportado apenas quando necessário e mantenha clientes compatíveis.
SuperJSON
É uma opção comum para serializar tipos adicionais. Avalie tamanho, segurança e compatibilidade de versão.
Não exponha modelos internos
Mesmo com tipos compartilhados, transforme entidades para DTOs. Não retorne hash de senha, tokens, flags internas ou colunas privadas.
Erros
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Recurso não encontrado'
});Use códigos estáveis e mensagens seguras. Não envie stack, SQL ou hostname.
Error formatter
É possível adicionar request ID e informações controladas:
errorFormatter({ shape, ctx }) {
return {
...shape,
data: {
...shape.data,
requestId: ctx?.requestId
}
};
}Erros de validação
Mapeie issues do schema sem expor detalhes internos. Campos podem receber códigos específicos para a interface.
CORS
Use allowlist de origens. Consulte CORS em APIs Node.js.
Cookies e CSRF
Se autenticação usa cookie, proteja mutations contra CSRF. SameSite ajuda, mas não substitui uma estratégia quando há uso cross-site.
JWT
Quando usar bearer token, valide assinatura, issuer, audience e expiração. Consulte JWT Seguro no Node.js.
Subscriptions
tRPC suporta atualizações em tempo real conforme adapter. A autenticação precisa ser validada na conexão e durante o ciclo quando permissões podem mudar.
WebSocket e SSE
Escolha transporte conforme bidirecionalidade e infraestrutura. Consulte WebSocket no Node.js.
Cancelamento
Propague AbortSignal para bancos e fetches quando possível. Se o cliente cancela, evite continuar trabalho caro sem necessidade.
Timeouts
Cada procedure deve respeitar um orçamento. Downstreams precisam de prazo menor que o timeout total.
Transações
Mutations que alteram várias tabelas devem delegar a uma camada de serviço transacional. Consulte Transações PostgreSQL no Node.js.
Idempotência
Mutations de pagamento e criação podem receber uma chave de idempotência. Veja Idempotência em APIs Node.js.
Cache
O cliente pode integrar com TanStack Query, mas o servidor ainda precisa definir consistência, invalidação e autorização.
Invalidation
Depois de mutation, invalide as queries relacionadas no cliente. No servidor, limpe caches compartilhados e loaders quando usados.
Monorepo
Exporte apenas o tipo do router para o frontend. Não importe módulos que executam conexão de banco ou carregam segredos.
Package de contrato
Em repositórios separados, um pacote pode publicar tipos. Isso exige versionamento e compatibilidade entre cliente e servidor.
Mudanças breaking
Renomear procedure, remover campo ou alterar input quebra clientes no build, mas clientes antigos já implantados podem continuar chamando a versão anterior.
Versionamento
Durante migração, mantenha procedure antiga e nova:
userById
userByIdV2Marque a antiga como depreciada no código e monitore uso.
API pública
Para terceiros, considere uma camada REST/OpenAPI ou GraphQL estável. O acoplamento TypeScript do tRPC é uma vantagem interna, não um contrato universal.
Logging
Registre procedure path, tipo, duração, status, request ID e tenant. Não registre inputs completos.
Consulte Logs com Pino no Node.js.
Métricas
- chamadas por procedure;
- latência p95 e p99;
- erros por código;
- tamanho de batch;
- rejeições de validação;
- timeouts;
- downstream latency.
Tracing
Crie span com procedure path e propague contexto para banco e APIs. Evite usar input como atributo.
Health check
Mantenha uma rota HTTP simples fora do tRPC para probes, ou uma procedure pública leve quando a infraestrutura permitir.
Graceful shutdown
Pare novas conexões, encerre subscriptions e aguarde requests em andamento. Consulte Graceful Shutdown no Node.js.
Testes de caller
const caller = appRouter.createCaller(testContext);
const user = await caller.userById({
id: userId
});Isso testa o router sem iniciar HTTP.
Teste de autenticação
Crie context sem usuário e confirme UNAUTHORIZED.
Teste de autorização
Use identidade sem permission e outro tenant. Ambos devem falhar sem acessar o repository fora do escopo.
Teste de validação
Envie UUID inválido, campos desconhecidos, strings grandes e listas acima do limite.
Teste HTTP
Além do caller, teste adapter, headers, cookies, CORS, batching e formato de erro.
Teste de compatibilidade
Compile um cliente de fixture contra o tipo exportado para detectar mudanças breaking.
Teste de carga
Meça batching, latência e downstreams. Um request HTTP com dez procedures ainda pode executar dez operações caras.
Erros comuns
- Confiar apenas no TypeScript: input da rede não é validado.
- Context global: identidade vaza entre requests.
- Router com regra de negócio: manutenção fica difícil.
- Batch sem limite: cliente contorna proteção.
- Expor modelo interno: campos privados chegam ao frontend.
- API externa acoplada: clientes de outras linguagens sofrem.
- Inputs em logs: dados sensíveis aparecem.
Boas práticas
- Valide todo input em runtime.
- Crie context por request.
- Use procedures protegidas.
- Autorize por recurso.
- Limite batches e listas.
- Separe serviços e routers.
- Não exponha entidades.
- Versione mudanças.
- Monitore por procedure.
- Teste caller e HTTP.
Conclusão
Usar tRPC no Node.js oferece uma experiência full-stack com tipos sincronizados e sem geração de código. Procedures e clientes permanecem alinhados durante o desenvolvimento.
A segurança continua dependendo de validação runtime, autenticação, autorização e limites. Com context por request, schemas estritos e observabilidade, tRPC reduz a duplicação de contratos sem transformar tipos de compilação em uma falsa barreira contra dados maliciosos.




