Skip to main content
Localizia

The tRPC API

Overview

The API is tRPC v11 with TanStack Query v5 on the client. Routers live in src/server/routers/ (one file per domain), merged in src/server/root.ts. Server code here is never imported by client components.

Procedure tiers

Always use the most restrictive tier that fits — the context it injects is guaranteed, so you never re-check it:

TierUse forInjects
publicProcedureUnauthenticated endpoints
protectedProcedureAny authenticated actionctx.user, ctx.session
protectedAdminProcedurePlatform-admin operations+ role === "admin"
protectedOrganizationProcedureOrg-scoped data+ ctx.organization, ctx.membership
protectedSharedOrganizationProcedureShared-org (non-personal) data+ shared-org context

Every input is validated

There are no exceptions — even a zero-input query declares a Zod schema. No raw input ever reaches a database query or external call:

getItem: protectedOrganizationProcedure
  .input(z.object({ id: z.string() }))
  .query(async ({ ctx, input }) => {
    return ctx.db.item.findFirst({
      where: { id: input.id, organizationId: ctx.organization.id },
    })
  })

Note the organizationId filter — mandatory on every org-scoped query.

Error handling

Throw TRPCError with a translation key as the message, never a raw English string. Catch external/Prisma errors at the router boundary, log them, and rethrow a generic error:

try {
  // …Prisma work…
} catch (err) {
  logger.error({ err, organizationId }, "items.create: failed")
  throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "errors.unexpected" })
}

The client receives the key and renders the translated string. Logging and Sentry reporting are handled by middleware in procedures.ts — don't add ad-hoc logging inside routers.

Calling the API

Client components use the generated hooks:

const { data } = api.items.list.useQuery({ ... })
const mutation = api.items.create.useMutation({
  onSuccess: () => utils.items.list.invalidate(),
})

Server Components use the server-side caller to prefetch — don't fetch your own endpoints:

const caller = await createCaller()
const items = await caller.items.list({ ... })

Never mix useEffect + fetch with tRPC for the same data — pick one.

Adding a router

  1. Create src/server/routers/[domain].router.ts, export it as default.
  2. Co-locate its Zod schemas in [domain].schema.ts (reuse them for form validation on the client).
  3. Merge it in src/server/root.ts.