Skip to main content

tRPC Overview

GTM Clarity uses tRPC 11 for end-to-end type-safe API communication. All client-server calls go through tRPC procedures with Zod input validation and automatic TypeScript inference.

Architecture

React Component
-> useTRPC() hook (type-safe client)
-> HTTP batch link (/api/trpc)
-> tRPC router (Zod validation + middleware)
-> Handler (DB queries, business logic)
-> JSON response (type-inferred)

Routers

The appRouter composes 9 domain-specific routers defined in src/server/trpc/router.ts:

export const appRouter = t.router({
tenant: tenantRouter,
audit: auditRouter,
user: userRouter,
connector: connectorRouter,
fieldMapping: fieldMappingRouter,
writeback: writebackRouter,
scoring: scoringRouter,
scoringConfig: scoringConfigRouter,
buyingGroup: buyingGroupRouter,
});

export type AppRouter = typeof appRouter;
RouterNamespaceKey ProceduresAccess
TenanttenantgetCurrent, updateSettingstenant / admin
Auditauditlist, getByResourceadmin
Useruserlist, getRoleadmin / tenant
Connectorconnectorlist, getStatus, getAuthUrl, triggerSynctenant / admin
Field MappingsfieldMappinglistSourceFields, getMappings, saveMappingstenant / admin
WritebackwritebackgetConfig, saveConfig, triggerWritebackadmin
ScoringscoringgetMatrixData, getEntityList, runScoringtenant / admin
Scoring ConfigscoringConfiggetConfig, updateConfig, resetConfigtenant / admin
Buying GroupsbuyingGrouplistByAccount, getDetail, getCandidatestenant / admin

Procedure Types

tRPC procedures are either queries (read-only, GET semantics) or mutations (write, POST semantics):

// Query -- fetches data, cacheable
getMatrixData: tenantProcedure
.input(z.object({ limit: z.number().default(500) }))
.query(async ({ ctx, input }) => { ... });

// Mutation -- modifies state
runScoring: adminProcedure
.input(z.object({ entityTypes: z.array(entityTypeSchema).optional() }))
.mutation(async ({ ctx, input }) => { ... });
TypeHTTP MethodUse ForCaching
queryGET (batched)Reading dataTanStack Query cache
mutationPOSTCreating, updating, deletingInvalidates related queries

Zod Input Validation

Every procedure validates its input with Zod 4 schemas. Invalid input returns a BAD_REQUEST error with structured validation details:

// Common schemas used across routers
const entityTypeSchema = z.enum(["person", "account", "opportunity"]);
const scoreTierSchema = z.enum(["hot", "warm", "cool", "cold"]);
const configTypeSchema = z.enum(["fit", "engagement", "combined"]);
Validation Errors

When Zod validation fails, tRPC returns a BAD_REQUEST error with the Zod issue array in the error cause. See Error Handling for details.

Client Setup

The tRPC client is initialized in src/lib/trpc.tsx using TanStack React Query:

"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createTRPCClient, httpBatchLink } from "@trpc/client";
import { createTRPCContext } from "@trpc/tanstack-react-query";
import { useState } from "react";
import type { AppRouter } from "@/server/trpc/router";

export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();

export function TRPCReactProvider({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient());
const [trpcClient] = useState(() =>
createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
}),
],
}),
);

return (
<QueryClientProvider client={queryClient}>
<TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
{children}
</TRPCProvider>
</QueryClientProvider>
);
}

Calling Procedures in React

Use the useTRPC hook to get type-safe access to all routers:

Queries

import { useTRPC } from "@/lib/trpc";
import { useQuery } from "@tanstack/react-query";

function EntityList() {
const trpc = useTRPC();

const { data, isLoading, error } = useQuery(
trpc.scoring.getEntityList.queryOptions({
entityType: "account",
page: 1,
pageSize: 25,
sortBy: "combinedScore",
sortDir: "desc",
})
);

if (error) return <ErrorDisplay error={error} />;
if (isLoading) return <Spinner />;
return <Table items={data.items} total={data.total} />;
}

Mutations

import { useTRPC } from "@/lib/trpc";
import { useMutation, useQueryClient } from "@tanstack/react-query";

function RunScoringButton() {
const trpc = useTRPC();
const queryClient = useQueryClient();

const mutation = useMutation(
trpc.scoring.runScoring.mutationOptions({
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: trpc.scoring.getMatrixData.queryKey() });
},
})
);

return (
<button
onClick={() => mutation.mutate({ entityTypes: ["person", "account"] })}
disabled={mutation.isPending}
>
{mutation.isPending ? "Scoring..." : "Run Scoring"}
</button>
);
}
Batch Requests

The httpBatchLink automatically batches multiple concurrent tRPC calls into a single HTTP request. No additional configuration is needed.