Use Cases (Atualizado: 02/06/2026)

API type-safe com tRPC e Claude Code

Crie uma API type-safe com Claude Code e tRPC: Next.js, Zod, permissões, cache invalidation e prompts de revisão.

API type-safe com tRPC e Claude Code

O que tRPC e Claude Code resolvem juntos

tRPC permite que o router TypeScript do servidor seja também o contrato usado pelo cliente. Em vez de manter um arquivo OpenAPI separado ou um SDK manual, o front-end importa o tipo AppRouter e recebe autocomplete para inputs, outputs e erros. Para equipes que mudam telas e endpoints na mesma base de código, isso reduz muito a divergência entre API e UI.

Claude Code ajuda quando é usado com contexto do repositório, não apenas para gerar boilerplate. Ele pode ler a autenticação existente, a organização das pastas, a camada de dados, os padrões de React Query e as regras de nomes. Com isso, consegue sugerir routers, procedures, schemas Zod, autorização e cache invalidation de forma consistente. Mesmo assim, type safety não substitui regras de negócio: uma mutation pode compilar e ainda permitir acesso indevido se faltar checagem de permissão.

Este artigo usa Next.js App Router e uma API pequena de gestão de projetos. Para manter o exemplo copiável, a persistência é um Map em memória. Em produção, troque por Prisma, Drizzle, Supabase ou sua camada de dados real, e substitua a sessão de demonstração por autenticação de verdade.

Casos de uso

tRPC funciona melhor quando servidor e cliente vivem no mesmo sistema TypeScript. Para APIs públicas de terceiros, OpenAPI pode ser mais adequado. Para painéis internos, ferramentas operacionais, formulários e BFFs finos, tRPC costuma acelerar bastante.

Caso de usoVantagem do tRPCO que pedir ao Claude CodeRisco
CRUD administrativoInputs e outputs compartilhados para listar, criar, editar e excluirRouter, Zod schema, mutations e invalidationEsconder botão sem autorização no servidor
Ferramentas internasMudanças rápidas sem manter SDK separadoProcedures a partir de modelos existentesContext grande demais
Envio de formuláriosValidação runtime de email, tamanho e enumErros claros e proteção contra duplo envioConfiar só em TypeScript
BFF finoResposta no formato exato da telaMapeamento de APIs externasCache e refetch indefinidos
flowchart LR
  UI["React component"]
  Client["tRPC React client"]
  Router["AppRouter"]
  Procedure["protected/admin procedure"]
  Zod["Zod validation"]
  Store["DB or store"]
  Review["Claude Code review"]

  UI --> Client --> Router --> Procedure --> Zod --> Store
  Router --> Review
  Review --> Procedure

Instalação e estrutura

Instale os pacotes principais. Se o projeto já usa React Query ou Zod, alinhe versões antes de gerar código.

npm install @trpc/server @trpc/client @trpc/react-query @tanstack/react-query zod superjson

Esta é a estrutura usada no exemplo. Código de servidor fica em src/server, a rota HTTP fica no App Router e o provider React fica em src/trpc.

src/
  app/api/trpc/[trpc]/route.ts
  app/projects/project-list.tsx
  server/trpc.ts
  server/routers/_app.ts
  server/routers/project.ts
  trpc/client.tsx

Ao pedir ajuda ao Claude Code, declare a fronteira: arquivos de servidor não importam client components, client components não acessam o store diretamente e os tipos compartilhados vêm do router tRPC, não de DTOs paralelos.

Context e procedures

O context leva informações por request para cada procedure. Mantenha pequeno: sessão, equipe, papel e acesso à base de dados geralmente bastam. O exemplo abaixo usa headers apenas para demonstração local.

// src/server/trpc.ts
import { initTRPC, TRPCError } from "@trpc/server";
import superjson from "superjson";

type Role = "admin" | "member";
type Session = { userId: string; teamId: string; role: Role };
export type Context = { session: Session | null };

export async function createContext({
  headers,
}: {
  headers: Headers;
}): Promise<Context> {
  const roleHeader = headers.get("x-user-role");
  const role: Role =
    roleHeader === "admin"
      ? "admin"
      : roleHeader === "member"
        ? "member"
        : process.env.NODE_ENV === "production"
          ? "member"
          : "admin";

  return {
    session: {
      userId: headers.get("x-user-id") ?? "demo-user",
      teamId: headers.get("x-team-id") ?? "demo-team",
      role,
    },
  };
}

const t = initTRPC.context<Context>().create({ transformer: superjson });

export const createTRPCRouter = t.router;
export const publicProcedure = t.procedure;

const requireUser = t.middleware(({ ctx, next }) => {
  if (!ctx.session) {
    throw new TRPCError({ code: "UNAUTHORIZED", message: "Login is required." });
  }
  return next({ ctx: { session: ctx.session } });
});

export const protectedProcedure = t.procedure.use(requireUser);

export const adminProcedure = protectedProcedure.use(({ ctx, next }) => {
  if (ctx.session.role !== "admin") {
    throw new TRPCError({ code: "FORBIDDEN", message: "Admin role is required." });
  }
  return next();
});

Separar protectedProcedure e adminProcedure cria um ponto claro de revisão. Se uma mutation destrutiva usar o procedure errado, o problema aparece logo no diff.

Router com validação Zod

TypeScript valida o código no build, mas não valida JSON recebido em runtime. Zod deve ficar na borda do procedure, antes da operação de negócio.

// src/server/routers/project.ts
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";

type ProjectStatus = "todo" | "doing" | "done";
type Project = {
  id: string;
  teamId: string;
  title: string;
  ownerEmail: string;
  status: ProjectStatus;
  createdAt: string;
};

const projects = new Map<string, Project>();
const projectStatus = z.enum(["todo", "doing", "done"]);

const listProjectsInput = z
  .object({
    status: projectStatus.optional(),
    query: z.string().trim().max(60).optional(),
    limit: z.number().int().min(1).max(50).default(20),
  })
  .default({ limit: 20 });

const createProjectInput = z.object({
  title: z.string().trim().min(2).max(80),
  ownerEmail: z.string().email(),
});

export const projectRouter = createTRPCRouter({
  list: protectedProcedure.input(listProjectsInput).query(({ ctx, input }) => {
    return [...projects.values()]
      .filter((project) => project.teamId === ctx.session.teamId)
      .filter((project) => !input.status || project.status === input.status)
      .filter((project) => !input.query || project.title.includes(input.query))
      .slice(0, input.limit);
  }),

  create: adminProcedure.input(createProjectInput).mutation(({ ctx, input }) => {
    const project: Project = {
      id: crypto.randomUUID(),
      teamId: ctx.session.teamId,
      title: input.title,
      ownerEmail: input.ownerEmail,
      status: "todo",
      createdAt: new Date().toISOString(),
    };
    projects.set(project.id, project);
    return project;
  }),

  updateStatus: protectedProcedure
    .input(z.object({ id: z.string().uuid(), status: projectStatus }))
    .mutation(({ ctx, input }) => {
      const project = projects.get(input.id);
      if (!project || project.teamId !== ctx.session.teamId) {
        throw new TRPCError({ code: "NOT_FOUND" });
      }
      const nextProject = { ...project, status: input.status };
      projects.set(input.id, nextProject);
      return nextProject;
    }),
});

A checagem de teamId é essencial. Um UUID válido não prova que o usuário atual pode alterar aquele registro. Type safety não cobre essa regra de negócio automaticamente.

App Router e cliente React

Monte o router principal e exponha o endpoint pelo Route Handler.

// src/server/routers/_app.ts
import { createTRPCRouter } from "../trpc";
import { projectRouter } from "./project";

export const appRouter = createTRPCRouter({
  project: projectRouter,
});

export type AppRouter = typeof appRouter;
// src/app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter } from "@/server/routers/_app";
import { createContext } from "@/server/trpc";

const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: "/api/trpc",
    req,
    router: appRouter,
    createContext: () => createContext({ headers: req.headers }),
  });

export { handler as GET, handler as POST };
// src/trpc/client.tsx
"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { httpBatchLink } from "@trpc/client";
import { createTRPCReact } from "@trpc/react-query";
import { useState, type ReactNode } from "react";
import superjson from "superjson";
import type { AppRouter } from "@/server/routers/_app";

export const trpc = createTRPCReact<AppRouter>();

export function TRPCProvider({ children }: { children: ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());
  const [trpcClient] = useState(() =>
    trpc.createClient({
      transformer: superjson,
      links: [httpBatchLink({ url: "/api/trpc" })],
    }),
  );

  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    </trpc.Provider>
  );
}
// src/app/projects/project-list.tsx
"use client";

import { useState } from "react";
import { trpc } from "@/trpc/client";

export function ProjectList() {
  const utils = trpc.useUtils();
  const [title, setTitle] = useState("");
  const projects = trpc.project.list.useQuery({ limit: 20 });
  const createProject = trpc.project.create.useMutation({
    onSuccess: async () => {
      setTitle("");
      await utils.project.list.invalidate();
    },
  });

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        createProject.mutate({ title, ownerEmail: "owner@example.com" });
      }}
    >
      <input value={title} onChange={(event) => setTitle(event.target.value)} />
      <button type="submit">Add</button>
      <pre>{JSON.stringify(projects.data, null, 2)}</pre>
    </form>
  );
}

Depois da mutation, utils.project.list.invalidate() força a lista a buscar dados novos. Sem essa regra, o usuário pode criar um item e continuar vendo dados antigos.

Prompt de revisão para Claude Code

Revise esta implementação de Next.js App Router + tRPC.
Verifique:
1. Uso correto de publicProcedure, protectedProcedure e adminProcedure.
2. Zod runtime validation para todo input externo.
3. Checagem de teamId e userId antes de ler ou escrever.
4. Invalidation das queries afetadas por mutations.
5. Routers separados por domínio e sem arquivos enormes.
6. Context apenas com dependências por request.
7. Nenhum campo secreto retornado ao cliente.

Retorne severidade, arquivo, problema, correção e testes recomendados em tabela.

Esse prompt pode entrar no CLAUDE.md ou no template de pull request. Em mudanças de permissão, peça primeiro o diff e a análise de risco antes de aplicar automaticamente.

Armadilhas comuns

A primeira armadilha é context inchado. Colocar tudo em ctx parece prático, mas dificulta testes. A segunda é autorização só na UI: esconder botão não protege endpoint. A terceira é confiar em TypeScript sem Zod; requests reais precisam de validação runtime. A quarta é não dividir routers; um arquivo único vira um gargalo de revisão. A quinta é cache invalidation mal definida; uma mutation pode afetar lista, detalhe, contador e dashboard.

Referências e próximos passos

Use a documentação oficial de tRPC para routers, procedures e React Query integration. Para validação runtime, consulte Zod. Para o endpoint no App Router, veja Next.js Route Handlers.

Para continuar, leia o guia de validação Zod com Claude Code e as dicas de TypeScript com Claude Code. Se sua equipe quer revisar um painel interno real, a consultoria e treinamento de Claude Code pode cobrir desenho de routers, permissões, prompts e testes.

Resultado ao testar

Em um protótipo de painel administrativo, o maior ganho veio de criar protectedProcedure e adminProcedure antes do CRUD. Claude Code conseguiu apontar mutations que precisavam de permissão de administrador. Deixar os schemas Zod perto das procedures também facilitou revisar mudanças de formulário. O ponto fraco apareceu quando o context cresceu demais; por isso a versão final manteve o context pequeno e moveu a lógica de negócio para funções separadas.

#Claude Code #tRPC #TypeScript #API #type safety
Grátis

PDF grátis: cheatsheet do Claude Code

Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.

Cuidamos dos seus dados e não enviamos spam.

Masa

Sobre o autor

Masa

Engenheiro focado em workflows práticos com Claude Code.