tRPC Overview
tRPC é o sistema principal de API do APAH Assistant, fornecendo comunicação type-safe entre cliente e servidor.
O que é tRPC?
tRPC permite construir APIs completamente type-safe sem schemas separados ou geração de código. Os tipos fluem automaticamente do servidor para o cliente.
// Servidor: definir procedure
const userRouter = createTRPCRouter({
getById: protectedProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
return db.query.users.findFirst({
where: eq(users.id, input.id),
});
}),
});
// Cliente: usar com tipos automáticos
const { data } = api.user.getById.useQuery({ id: "123" });
// data é automaticamente tipado como User | undefined
Configuração
Setup do Servidor
// src/server/api/trpc.ts
import { initTRPC, TRPCError } from "@trpc/server";
import superjson from "superjson";
import { db } from "@/server/db";
import { auth } from "@/server/auth";
// Contexto disponível em todas as procedures
const createContext = async (opts: { headers: Headers }) => {
const session = await auth.api.getSession({
headers: opts.headers,
});
return {
db,
session,
};
};
const t = initTRPC.context<typeof createContext>().create({
transformer: superjson,
errorFormatter({ shape }) {
return shape;
},
});
export const createTRPCRouter = t.router;
export const publicProcedure = t.procedure;
Procedures Protegidas
// Procedure que requer autenticação
export const protectedProcedure = t.procedure.use(async ({ ctx, next }) => {
if (!ctx.session?.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: {
session: ctx.session,
user: ctx.session.user,
},
});
});
// Procedure que requer role de admin
export const adminProcedure = protectedProcedure.use(async ({ ctx, next }) => {
const isAdmin = ctx.user.roles?.some(r => r.name === "admin");
if (!isAdmin) {
throw new TRPCError({ code: "FORBIDDEN" });
}
return next({ ctx });
});
Router Raiz
// src/server/api/root.ts
import { createTRPCRouter } from "./trpc";
import { userRouter } from "./routers/user";
import { chatRouter } from "./routers/chat";
import { libraryRouter } from "./routers/library";
export const appRouter = createTRPCRouter({
user: userRouter,
chat: chatRouter,
library: libraryRouter,
});
export type AppRouter = typeof appRouter;
Cliente
Setup do Provider
// src/app/providers.tsx
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { httpBatchLink } from "@trpc/client";
import { api } from "@/lib/trpc/client";
import superjson from "superjson";
const queryClient = new QueryClient();
const trpcClient = api.createClient({
links: [
httpBatchLink({
url: "/api/trpc",
transformer: superjson,
}),
],
});
export function Providers({ children }: { children: React.ReactNode }) {
return (
<api.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</api.Provider>
);
}
Uso no Cliente
"use client";
import { api } from "@/lib/trpc/client";
function UserList() {
// Query
const { data, isLoading, error } = api.user.list.useQuery({
page: 1,
limit: 10,
});
// Mutation
const createUser = api.user.create.useMutation({
onSuccess: () => {
// Invalidar cache
utils.user.list.invalidate();
},
});
// Utils para invalidação
const utils = api.useUtils();
if (isLoading) return <Loading />;
if (error) return <Error message={error.message} />;
return (
<div>
{data.items.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
);
}
Validação com Zod
Todas as inputs são validadas com Zod:
import { z } from "zod";
const createUserSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
password: z.string().min(8),
roleId: z.string(),
});
export const userRouter = createTRPCRouter({
create: adminProcedure
.input(createUserSchema)
.mutation(async ({ ctx, input }) => {
// input é automaticamente validado e tipado
return ctx.db.insert(users).values(input);
}),
});
Tipos de Procedures
Query
Para leitura de dados:
getById: protectedProcedure
.input(z.object({ id: z.string() }))
.query(async ({ ctx, input }) => {
return ctx.db.query.users.findFirst({
where: eq(users.id, input.id),
});
}),
Mutation
Para escrita de dados:
update: protectedProcedure
.input(z.object({
id: z.string(),
name: z.string().optional(),
email: z.string().email().optional(),
}))
.mutation(async ({ ctx, input }) => {
const { id, ...data } = input;
return ctx.db.update(users)
.set(data)
.where(eq(users.id, id));
}),
Subscription (não usado atualmente)
Para dados em tempo real via WebSocket.
Tratamento de Erros
import { TRPCError } from "@trpc/server";
// Lançar erros
throw new TRPCError({
code: "NOT_FOUND",
message: "Utilizador não encontrado",
});
// Códigos disponíveis
type ErrorCode =
| "PARSE_ERROR"
| "BAD_REQUEST"
| "UNAUTHORIZED"
| "FORBIDDEN"
| "NOT_FOUND"
| "CONFLICT"
| "INTERNAL_SERVER_ERROR";
Tratamento no Cliente
const mutation = api.user.create.useMutation({
onError: (error) => {
if (error.data?.code === "CONFLICT") {
toast.error("Email já existe");
} else {
toast.error(error.message);
}
},
});
Batching
tRPC automaticamente agrupa múltiplas queries:
// Estas queries são enviadas numa única request HTTP
const { data: users } = api.user.list.useQuery();
const { data: documents } = api.library.list.useQuery();
const { data: session } = api.auth.getSession.useQuery();
Optimistic Updates
const utils = api.useUtils();
const mutation = api.user.update.useMutation({
onMutate: async (newData) => {
// Cancelar queries em andamento
await utils.user.getById.cancel({ id: newData.id });
// Snapshot do valor anterior
const previousUser = utils.user.getById.getData({ id: newData.id });
// Atualização optimista
utils.user.getById.setData({ id: newData.id }, (old) => ({
...old!,
...newData,
}));
return { previousUser };
},
onError: (err, newData, context) => {
// Reverter em caso de erro
utils.user.getById.setData(
{ id: newData.id },
context!.previousUser
);
},
onSettled: () => {
// Invalidar para sincronizar com servidor
utils.user.getById.invalidate();
},
});
Server Components
Usar tRPC em React Server Components:
// src/app/users/page.tsx
import { api } from "@/lib/trpc/server";
export default async function UsersPage() {
const users = await api.user.list({ page: 1, limit: 10 });
return (
<div>
{users.items.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
);
}