Type-safe API com tRPC
1. Introdução ao tRPC e o Problema da Tipagem em APIs
Manter a consistência de tipos entre frontend e backend é um dos maiores desafios no desenvolvimento web moderno. Em projetos tradicionais, você define schemas no backend, depois os reescreve no frontend, e qualquer alteração exige sincronização manual — um processo propenso a erros que quebra aplicações em produção.
tRPC (TypeScript Remote Procedure Call) surge como uma alternativa radical: ele permite que você defina procedimentos no backend e os consuma no frontend com inferência total de tipos, sem geradores de código, schemas duplicados ou contratos externos.
Comparado a outras abordagens:
- REST + Zod: Você ainda precisa gerenciar endpoints e schemas separadamente, mesmo com validação.
- GraphQL: Oferece tipagem forte, mas exige schemas GraphQL, resolvers e ferramentas como GraphQL Code Generator.
- OpenAPI: Gera tipos, mas a manutenção do schema YAML/JSON é manual e frequentemente dessincronizada.
tRPC elimina essas camadas intermediárias, aproveitando o TypeScript como única fonte de verdade.
2. Arquitetura Fundamental do tRPC
O núcleo do tRPC é baseado em procedimentos — funções que podem ser queries (leitura), mutations (escrita) ou subscriptions (tempo real). Esses procedimentos são organizados em routers, que formam uma árvore tipada.
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
const appRouter = t.router({
hello: t.procedure
.input(z.object({ name: z.string() }))
.query(({ input }) => {
return { greeting: `Hello, ${input.name}!` };
}),
});
O contexto (createContext) permite injetar dependências tipadas (como conexão com banco de dados ou usuário autenticado) em todos os procedimentos.
export const createContext = async () => {
const user = await getUserFromRequest();
return { user };
};
const t = initTRPC.context<typeof createContext>().create();
3. Definindo Procedimentos com Inferência de Tipos
A validação de entrada com Zod não apenas garante segurança em runtime, mas também fornece inferência automática de tipos para o TypeScript.
import { z } from 'zod';
const userSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
age: z.number().min(0).max(150),
});
const userRouter = t.router({
create: t.procedure
.input(userSchema.omit({ id: true }))
.mutation(async ({ input, ctx }) => {
// input é tipado automaticamente como { email: string; age: number }
const user = await ctx.db.user.create({ data: input });
return user; // O tipo de retorno é inferido e propagado para o cliente
}),
getById: t.procedure
.input(z.string().uuid())
.query(async ({ input, ctx }) => {
return ctx.db.user.findUnique({ where: { id: input } });
}),
});
O TypeScript propaga esses tipos automaticamente para o cliente — sem declarações manuais, sem as casts, sem interface duplicadas.
4. Consumo Tipado no Frontend com @trpc/client
No frontend, a configuração é mínima e a inferência é total:
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/router';
export const trpc = createTRPCReact<AppRouter>();
O uso em componentes React é direto e totalmente tipado:
function UserProfile({ userId }: { userId: string }) {
// Tipagem automática de parâmetros e resposta
const { data, isLoading } = trpc.user.getById.useQuery(userId);
const createUser = trpc.user.create.useMutation({
onSuccess: (newUser) => {
// newUser é tipado automaticamente como o retorno da mutation
console.log(newUser.email);
},
});
if (isLoading) return <div>Loading...</div>;
return (
<div>
<h1>{data?.email}</h1>
<button onClick={() => createUser.mutate({ email: 'test@test.com', age: 25 })}>
Create User
</button>
</div>
);
}
Se você alterar o schema no backend, o TypeScript apontará todos os locais no frontend que precisam ser atualizados — em tempo de compilação.
5. Tratamento de Erros e Respostas Complexas
tRPC fornece erros tipados com TRPCError:
import { TRPCError } from '@trpc/server';
const userRouter = t.router({
getById: t.procedure
.input(z.string().uuid())
.query(async ({ input, ctx }) => {
const user = await ctx.db.user.findUnique({ where: { id: input } });
if (!user) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'User not found',
});
}
return user;
}),
});
Para serialização de tipos complexos como Date, Map ou Set, utilize superjson:
import superjson from 'superjson';
const t = initTRPC.create({
transformer: superjson,
});
// Agora você pode retornar Date diretamente
const eventRouter = t.router({
getEvents: t.procedure.query(async () => {
return [
{ title: 'Meeting', date: new Date('2024-01-15') },
];
}),
});
6. Autenticação e Autorização com Types
Middlewares no tRPC podem modificar o contexto e adicionar informações tipadas:
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({
ctx: {
user: ctx.user, // Agora tipado como não-nulo
},
});
});
const protectedProcedure = t.procedure.use(isAuthed);
const protectedRouter = t.router({
getProfile: protectedProcedure.query(({ ctx }) => {
// ctx.user é garantido como não-nulo pelo middleware
return ctx.user;
}),
});
Isso permite expor informações do usuário no contexto sem quebrar a segurança de tipos — o middleware garante que procedimentos protegidos sempre tenham ctx.user disponível.
7. Boas Práticas e Padrões Avançados
Organize routers em módulos e faça merge:
// routers/user.ts
export const userRouter = t.router({ ... });
// routers/post.ts
export const postRouter = t.router({ ... });
// routers/index.ts
export const appRouter = t.router({
user: userRouter,
post: postRouter,
});
export type AppRouter = typeof appRouter;
Crie pipelines de validação reutilizáveis:
const validatePagination = t.middleware(({ input, next }) => {
const { page, limit } = input as { page?: number; limit?: number };
return next({
input: { ...input, page: page ?? 1, limit: limit ?? 10 },
});
});
const paginatedProcedure = t.procedure.use(validatePagination);
Para code splitting no servidor, utilize lazy loading de routers:
const appRouter = t.router({
admin: lazy(() => import('./routers/admin').then(m => m.adminRouter)),
});
8. Testes, Monitoramento e Deploy
Testes unitários com procedimentos tipados são diretos:
import { createCallerFactory } from '@trpc/server';
const createCaller = createCallerFactory(appRouter);
test('create user', async () => {
const caller = createCaller({ user: { id: '1', role: 'ADMIN' } });
const result = await caller.user.create({ email: 'test@test.com', age: 30 });
expect(result.email).toBe('test@test.com');
});
Para monitoramento, utilize hooks do tRPC:
const t = initTRPC.create();
t.middleware(async ({ path, type, next }) => {
const start = Date.now();
const result = await next();
const duration = Date.now() - start;
console.log(`Procedure ${path} (${type}) took ${duration}ms`);
return result;
});
Para deploy, escolha o adapter adequado:
// Next.js
export default trpcNext.createNextApiHandler({ router: appRouter, createContext });
// Express
import * as trpcExpress from '@trpc/server/adapters/express';
app.use('/trpc', trpcExpress.createExpressMiddleware({ router: appRouter, createContext }));
// Fastify
import * as trpcFastify from '@trpc/server/adapters/fastify';
fastify.register(trpcFastify.fastifyTRPCPlugin, { router: appRouter, createContext });
tRPC transforma a maneira como construímos APIs full-stack com TypeScript. Ao eliminar a duplicação de tipos e fornecer inferência automática, ele reduz bugs, acelera o desenvolvimento e torna a refatoração segura. Se você já investe em TypeScript no frontend e backend, tRPC é o próximo passo lógico para uma experiência de desenvolvimento verdadeiramente integrada.
Referências
- Documentação oficial do tRPC — Guia completo com exemplos de configuração, procedimentos, middlewares e adapters para diferentes frameworks.
- tRPC + React Query Quickstart — Tutorial oficial para integrar tRPC com React Query no frontend, incluindo uso de hooks tipados.
- Zod + tRPC: Validação de entrada — Documentação sobre como usar Zod para validar inputs e inferir tipos automaticamente no tRPC.
- tRPC Authentication Middleware — Exemplos práticos de como implementar autenticação e autorização com middlewares tipados.
- tRPC Error Handling Guide — Guia oficial sobre TRPCError, códigos HTTP e tratamento de erros tipados no servidor e cliente.
- tRPC with SuperJSON — Documentação sobre serialização de tipos complexos (Date, Map) usando superjson como transformer.
- tRPC Testing Strategies — Guia oficial para testar procedimentos tRPC com chamadas diretas e mocks de contexto.