<h2>O Problema das APIs Tradicionais</h2>
<p>Quando desenvolvemos aplicações modernas com TypeScript tanto no frontend quanto no backend, enfrentamos um desafio recorrente: manter a tipagem sincronizada entre cliente e servidor. As abordagens tradicionais (REST com Swagger, GraphQL com geração de código) exigem um passo intermediário de geração de tipos ou documentação manual que frequentemente fica desatualizada.</p>
<p>Imagine uma situação comum: você altera um endpoint no backend, adicionando um novo campo a uma resposta. O frontend continua usando os tipos antigos até que alguém manualmente atualize o arquivo de tipos gerado. Este descompasso é a raiz de muitos bugs em produção. O tRPC resolve isso de forma elegante: em vez de serializar dados em JSON e perder informações de tipo no caminho, ele transmite apenas os dados essenciais enquanto mantém a tipagem TypeScript pura em ambos os lados.</p>
<h2>Entendendo o tRPC</h2>
<h3>O Conceito Fundamental</h3>
<p>tRPC significa "TypeScript Remote Procedure Call" — é um framework que permite chamar funções do servidor diretamente do cliente com segurança de tipo total, sem necessidade de gerar código intermediário. Diferentemente de REST ou GraphQL, tRPC explora a natureza do TypeScript para oferecer type-safety automática.</p>
<p>A magia acontece porque tanto o cliente quanto o servidor compartilham a mesma definição de tipos. Quando você define um procedimento no servidor usando tRPC, você está simultaneamente criando a interface que o cliente usará. Não há arquivo de esquema separado, não há geração de código — apenas TypeScript puro.</p>
<pre><code class="language-typescript">// backend/router.ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const router = t.router({
user: t.procedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
const user = await db.user.findUnique({
where: { id: input.id }
});
return {
id: user.id,
name: user.name,
email: user.email
};
}),
});
export type AppRouter = typeof router;</code></pre>
<h3>Por Que Funciona Sem Geração de Código</h3>
<p>A resposta está no sistema de tipos do TypeScript. Quando você exporta <code>AppRouter</code>, você está exportando os tipos da sua API. No frontend, você importa esse tipo e usa para criar um cliente tipado:</p>
<pre><code class="language-typescript">// frontend/client.ts
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from '../backend/router';
const trpc = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000/trpc',
}),
],
});
// Agora qualquer chamada é totalmente tipada
const user = await trpc.user.query({ id: '123' });
// TypeScript sabe que 'user' tem as propriedades: id, name, email</code></pre>
<p>Aqui está o ponto crítico: o tipo <code>AppRouter</code> é apenas informação de tempo de compilação. Ele não gera nenhum código JavaScript extra. O cliente sabe exatamente qual forma de dados esperar porque tem acesso aos mesmos tipos que o servidor usou ao definir os procedimentos.</p>
<h2>Construindo uma API Completa com tRPC</h2>
<h3>Estrutura de um Servidor tRPC</h3>
<p>Um servidor tRPC é construído em cima de um framework HTTP (Express, Next.js, etc.) e expõe um router com procedimentos. Existem três tipos de procedimentos: queries (leitura), mutations (escrita) e subscriptions (tempo real).</p>
<pre><code class="language-typescript">// server.ts
import { initTRPC } from '@trpc/server';
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { z } from 'zod';
const t = initTRPC.create();
interface User {
id: string;
name: string;
email: string;
}
const users: User[] = [
{ id: '1', name: 'Alice', email: 'alice@example.com' },
{ id: '2', name: 'Bob', email: 'bob@example.com' },
];
export const appRouter = t.router({
// Query: buscar usuário
getUser: t.procedure
.input(z.object({ id: z.string() }))
.query(({ input }) => {
return users.find(u => u.id === input.id);
}),
// Query: listar todos os usuários
listUsers: t.procedure
.query(() => {
return users;
}),
// Mutation: criar usuário
createUser: t.procedure
.input(z.object({
name: z.string(),
email: z.string().email(),
}))
.mutation(({ input }) => {
const newUser: User = {
id: String(users.length + 1),
...input,
};
users.push(newUser);
return newUser;
}),
// Mutation: atualizar usuário
updateUser: t.procedure
.input(z.object({
id: z.string(),
name: z.string().optional(),
email: z.string().email().optional(),
}))
.mutation(({ input }) => {
const user = users.find(u => u.id === input.id);
if (!user) throw new Error('Usuário não encontrado');
if (input.name) user.name = input.name;
if (input.email) user.email = input.email;
return user;
}),
});
export type AppRouter = typeof appRouter;
// Criar servidor HTTP
const server = createHTTPServer({
router: appRouter,
});
server.listen(3000, () => {
console.log('tRPC server running on http://localhost:3000');
});</code></pre>
<p>Note que usamos <code>zod</code> para validação de entrada. Isso garante que os dados que chegam do cliente correspondem ao esperado, e o TypeScript infere automaticamente os tipos de entrada e saída.</p>
<h3>Cliente Consumindo a API</h3>
<p>No lado do cliente, temos acesso a todos os procedimentos com autocompletar automático:</p>
<pre><code class="language-typescript">// client.ts
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from './server';
const trpc = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000',
}),
],
});
async function main() {
// Buscar um usuário
const user = await trpc.getUser.query({ id: '1' });
console.log(user); // { id: '1', name: 'Alice', email: 'alice@example.com' }
// Listar todos
const allUsers = await trpc.listUsers.query();
console.log(allUsers);
// Criar novo usuário
const newUser = await trpc.createUser.mutation({
name: 'Charlie',
email: 'charlie@example.com',
});
console.log(newUser);
// Atualizar usuário
const updated = await trpc.updateUser.mutation({
id: '1',
name: 'Alice Updated',
});
console.log(updated);
}
main();</code></pre>
<p>Se você tentar passar dados inválidos ou acessar um procedimento que não existe, o TypeScript reclamará imediatamente:</p>
<pre><code class="language-typescript"></code></pre>
<h2>Padrões Avançados e Boas Práticas</h2>
<h3>Reutilizando Lógica com Middlewares</h3>
<p>tRPC oferece middlewares que executam antes de cada procedimento, permitindo adicionar autenticação, logging ou autorização:</p>
<pre><code class="language-typescript">import { initTRPC, TRPCError } from '@trpc/server';
const t = initTRPC
.context<{ userId?: string }>()
.create();
// Middleware de autenticação
const isAuthenticated = t.middleware(({ ctx, next }) => {
if (!ctx.userId) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({
ctx: {
userId: ctx.userId,
},
});
});
// Criar procedures protegidas
const protectedProcedure = t.procedure.use(isAuthenticated);
export const appRouter = t.router({
// Procedure pública
getPublicData: t.procedure
.query(() => ({ message: 'Dados públicos' })),
// Procedure protegida
getPrivateData: protectedProcedure
.query(({ ctx }) => {
return { message: Dados privados do usuário ${ctx.userId} };
}),
// Mutation protegida
updateProfile: protectedProcedure
.input(z.object({ name: z.string() }))
.mutation(({ ctx, input }) => {
return { userId: ctx.userId, name: input.name };
}),
});</code></pre>
<h3>Composição de Routers</h3>
<p>Em aplicações maiores, é comum dividir os procedimentos em múltiplos routers. tRPC permite compor routers facilmente:</p>
<pre><code class="language-typescript">// users.ts
const usersRouter = t.router({
list: t.procedure.query(() => users),
getById: t.procedure
.input(z.string())
.query(({ input }) => users.find(u => u.id === input)),
});
// posts.ts
const postsRouter = t.router({
list: t.procedure.query(() => posts),
create: t.procedure
.input(z.object({ title: z.string(), userId: z.string() }))
.mutation(({ input }) => {
const post = { id: generateId(), ...input };
posts.push(post);
return post;
}),
});
// router.ts (composição)
export const appRouter = t.router({
user: usersRouter,
post: postsRouter,
});</code></pre>
<p>No cliente, a estrutura se reflete automaticamente:</p>
<pre><code class="language-typescript">// Acesso aos procedures via namespaces
await trpc.user.list.query();
await trpc.user.getById.query('123');
await trpc.post.create.mutation({ title: 'Hello', userId: '1' });</code></pre>
<h3>Tratamento de Erros</h3>
<p>tRPC oferece uma forma estruturada de lidar com erros com códigos específicos:</p>
<pre><code class="language-typescript">import { TRPCError } from '@trpc/server';
const appRouter = t.router({
deleteUser: t.procedure
.input(z.string())
.mutation(({ input }) => {
const user = users.find(u => u.id === input);
if (!user) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Usuário não encontrado',
});
}
if (user.isAdmin) {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'Não é possível deletar um admin',
});
}
const index = users.indexOf(user);
users.splice(index, 1);
return { success: true };
}),
});
// No cliente, você pode tratar erros assim:
try {
await trpc.deleteUser.mutation('123');
} catch (error) {
if (error.code === 'NOT_FOUND') {
console.log('Usuário não existe');
} else if (error.code === 'FORBIDDEN') {
console.log('Não tem permissão');
}
}</code></pre>
<h2>Comparação com Alternativas</h2>
<h3>REST vs tRPC</h3>
<p>REST tradicional exige que você defina endpoints e tipos separadamente. Se mudar um endpoint, precisa atualizar documentação e gerar novos tipos no cliente:</p>
<pre><code class="language-typescript">// REST (sem type-safety)
const response = await fetch('/api/users/123');
const user = await response.json();
// TypeScript não sabe qual é a forma de 'user'
console.log(user.name); // Pode quebrar em runtime
// tRPC (com type-safety)
const user = await trpc.getUser.query({ id: '123' });
console.log(user.name); // TypeScript garante que existe</code></pre>
<h3>GraphQL vs tRPC</h3>
<p>GraphQL oferece um modelo declarativo poderoso, mas exige geração de código ou runtime introspection. tRPC é mais simples para aplicações TypeScript monorepo:</p>
<pre><code class="language-typescript">// GraphQL requer query string e geração de tipos
const query = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
}
}
`;
const result = await client.query({ query, variables: { id: '123' } });
// tRPC é apenas uma chamada de função tipada
const user = await trpc.getUser.query({ id: '123' });</code></pre>
<p>tRPC não é melhor que GraphQL — é uma alternativa mais leve quando você controla ambos os lados da comunicação (monorepo TypeScript).</p>
<h2>Conclusão</h2>
<p>Aprendemos que <strong>tRPC elimina o gap de tipagem entre cliente e servidor</strong> sem adicionar passos de geração de código, porque explore o sistema de tipos do TypeScript de forma criativa. Você define os tipos uma vez no servidor, exporta-os, e o cliente os importa — simples, mas poderoso.</p>
<p>Também vimos que <strong>tRPC é ideal para monorepos TypeScript</strong> onde você controla toda a stack. Ao usar middlewares, composição de routers e tratamento de erros estruturado, você constrói APIs robustas com a mesma segurança de tipo que teria em código local.</p>
<p>Por fim, compreendemos que <strong>a escolha entre tRPC, REST e GraphQL depende do contexto</strong>: use tRPC para rapidez e type-safety em monorepos, REST para APIs públicas que serão consumidas por múltiplas linguagens, e GraphQL para APIs complexas com queries muito variadas.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://trpc.io/docs" target="_blank" rel="noopener noreferrer">tRPC Documentation</a> — Documentação oficial completa</li>
<li><a href="https://github.com/trpc/trpc" target="_blank" rel="noopener noreferrer">tRPC GitHub Repository</a> — Código-fonte e exemplos</li>
<li><a href="https://www.typescriptlang.org/docs/" target="_blank" rel="noopener noreferrer">TypeScript Handbook</a> — Fundamentos do TypeScript</li>
<li><a href="https://zod.dev" target="_blank" rel="noopener noreferrer">Zod Validation Library</a> — Documentação para validação de dados</li>
<li><a href="https://www.youtube.com/watch?v=2LYM8gf184U" target="_blank" rel="noopener noreferrer">Building Type-Safe APIs with tRPC</a> — Talk introdutória sobre o tema</li>
</ul>
<p><!-- FIM --></p>