How to generate app-router-ready trpc procedures from excel data schemas
- Step 1Match headers to your entity columns — Use a sheet whose first row is the entity's field names. They become the Zod fields and the TS interface, camelCased. Keep sample rows so type inference can read numbers, booleans, and ISO dates.
- Step 2Open the tool on a Developer plan — It's Developer-tier; otherwise you get an upgrade overlay and the error
tRPC Router Builder requires Developer tier.Drop the.xlsx/.csv— SheetJS parses it locally. - Step 3Name the router with the entityNameRouter convention — Use
postsRouter,commentsRouter, etc. For a single sheet the entity name is derived (Post,Comment); for multiple sheets the name becomes the root router and each tab is a sub-router named after the sheet. - Step 4Place the file in server/api/routers/ — Download
<routerName>.tsinto your routers directory. Then fix the../trpcimport to point at where your App Router project defines its tRPC instance (often@/server/api/trpcor a relative path). - Step 5Register it on the root appRouter — Import the generated router and add it to
appRouterinserver/api/root.ts. For a multi-sheet root router, mount the whole thing or splice its sub-routers into your existing root. - Step 6Wire protectedProcedure and the data layer — Ensure
protectedProcedureexists in your tRPC setup with your real auth guard (NextAuth/Clerk/Auth.js). Replace thectx.db.<entity>.*placeholders with your Prisma/Drizzle calls. Then consume the router via tRPC's App Router server/client helpers.
Procedure access model (App Router)
The read/write split is fixed by the generator. publicProcedure and protectedProcedure must already be defined in your tRPC instance.
| Procedure | Generated as | Wire to |
|---|---|---|
list | publicProcedure | Public read; swap if your list must be auth-gated |
getById | publicProcedure | Public read; throws TRPCError NOT_FOUND |
create | protectedProcedure | Your auth-guarded procedure (NextAuth / Clerk / Auth.js) |
update | protectedProcedure | Same auth guard |
delete | protectedProcedure | Same auth guard |
Imports and integration points
What the generated file imports and what you must adjust for your project.
| Generated line / artifact | You adjust |
|---|---|
import { z } from "zod"; | Usually nothing |
import { router, publicProcedure, protectedProcedure } from "../trpc"; | Fix the path to your tRPC instance (e.g. @/server/api/trpc) |
import { TRPCError } from "@trpc/server"; | Usually nothing |
ctx.db.<entity>.findMany / findUnique / create / update / delete | Replace with your Prisma/Drizzle queries |
export const <routerName> = router({ ... }) | Register on appRouter in server/api/root.ts |
export type <Entity>Router = typeof <routerName>; | Optional — handy for client-side typing |
Inputs, output, and tier limits
Developer-only; single workbook in, one TypeScript router out.
| Property | Value |
|---|---|
| Input formats | .xlsx, .xls, .ods, .csv |
| Output | One .ts file (<routerName>.ts) |
| tRPC target | v11; ../trpc + @trpc/server imports |
| Options | routerName only (default dataRouter) |
| Required tier | Developer |
| Developer file / rows | 500 MB / unlimited |
| Multi-entity | Multi-tab workbook → root router + sub-routers |
Cookbook
Wiring the generated router into a create-t3-app / Next.js App Router project, with the manual steps spelled out.
A posts router for the App Router
A Posts sheet generates postsRouter with public reads and protected writes. Fix the ../trpc import and register on appRouter.
Input (sheet: Posts):
id,title,slug,published
1,Hello World,hello-world,true
Router name: postsRouter
Generated postsRouter.ts (top):
import { z } from "zod";
import { router, publicProcedure, protectedProcedure } from "../trpc";
import { TRPCError } from "@trpc/server";
// ...PostSchema, postCreateSchema, postUpdateSchema...
export const postsRouter = router({ list, getById, create, update, delete });
export type PostRouter = typeof postsRouter;
// 1) place in src/server/api/routers/posts.ts
// 2) fix import path -> from "@/server/api/trpc"
// 3) register on appRouterRegister on the root appRouter
Mount the generated router in server/api/root.ts so the App Router server/client helpers can reach it.
// src/server/api/root.ts
import { createTRPCRouter } from "@/server/api/trpc";
import { postsRouter } from "@/server/api/routers/posts";
export const appRouter = createTRPCRouter({
posts: postsRouter,
});
export type AppRouter = typeof appRouter;protectedProcedure is a placeholder you must define
The generated mutations use protectedProcedure, but the generator does not create it. It has to exist in your tRPC instance with your real auth middleware.
// In your @/server/api/trpc.ts (you own this):
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session?.user) throw new TRPCError({ code: "UNAUTHORIZED" });
return next({ ctx: { ...ctx, session: ctx.session } });
});
// The generated router just references it; if it's missing the file won't compile.Consume from a Server Component and a Client hook
Once registered, the App Router helpers give you type-safe access on both the server and the client.
// Server Component (RSC):
const { data } = await api.posts.list({ page: 1, pageSize: 20 });
// Client Component:
const { data } = api.posts.list.useQuery({ page: 1, pageSize: 20, search: "hello" });
// Both infer types from the generated router -> no manual type annotations.Multi-entity API from one workbook
Tabs for Posts and Comments produce a root router; splice its sub-routers into your existing appRouter.
Workbook tabs: Posts | Comments
Router name: contentRouter
export const contentRouter = router({
posts: postsRouter,
comments: commentsRouter,
});
// Mount whole thing:
export const appRouter = createTRPCRouter({ content: contentRouter });
// or splice: createTRPCRouter({ posts: ..., comments: ... })Edge cases and what actually happens
The ../trpc import path won't resolve
Manual fixThe generated file imports from the literal relative path ../trpc. In a create-t3-app project the tRPC instance is usually at @/server/api/trpc (with createTRPCRouter, not a bare router). Adjust the import path — and the router vs createTRPCRouter name — to match your project, or the file won't compile.
protectedProcedure / publicProcedure don't exist
Compile errorThe generator references protectedProcedure and publicProcedure but does not define them. They must already exist in your tRPC instance with your auth middleware wired in. If your project names them differently (or only has publicProcedure), rename the references after generating.
Developer tier required
Developer requiredOn Free / Pro / Pro+Media the tool throws tRPC Router Builder requires Developer tier. and shows an upgrade overlay. Generate on a Developer-tier account.
list is public but should be auth-gated
Review accessReads are generated as publicProcedure. If your App Router screen requires auth for listing (e.g. an admin table), change list/getById to protectedProcedure after generating. The generator can't know which reads are public.
getById / delete id fixed to z.string().uuid()
By designThose inputs are hard-coded to z.string().uuid() regardless of your id column. For a Prisma Int @id or a CUID, change the validator (z.number().int() or z.string()) so your App Router mutations accept the real id type.
Date fields generated as z.string()
By designISO YYYY-MM-DD columns stay z.string(), not z.date(). If a Server Action or form passes a Date, coerce with z.coerce.date() after generating, or convert in the resolver before the ctx.db call.
Sub-router names come from sheet names
Multi-sheet behaviourFor a multi-tab workbook each sub-router/entity name is taken from its sheet name, not the router name. Name tabs to your App Router entities (Posts, Comments) before generating.
Headers must be in the first row
Headers from row 1SheetJS reads row 1 as field names. A title/metadata row above your schema will be parsed as fields. Remove any preamble so row 1 holds only column names.
search covers only the first two string columns
By designThe list search filter ORs over the first two z.string() columns. For an App Router search UI that should cover more fields, extend the generated where clause manually.
Output preview truncated at 4,000 chars
Preview onlyLarge routers are cut off in the on-page preview. Use Copy (full string) or Download to get the complete file before pasting into your routers directory.
Frequently asked questions
Does the generated code follow create-t3-app conventions?
Largely. It imports router, publicProcedure, and protectedProcedure and splits reads/writes the T3 way. The differences to fix: T3 typically exports createTRPCRouter (not a bare router) from @/server/api/trpc, so adjust the import name and path after generating.
What auth guard pattern is used?
Mutations use protectedProcedure as a placeholder — the generator references it but does not define it. You wire your real auth middleware (NextAuth, Clerk, Auth.js) into protectedProcedure in your tRPC instance. If it's missing, the file won't compile.
Where do I put the generated file?
In your tRPC routers directory (commonly src/server/api/routers/), then register it on the root appRouter in server/api/root.ts. After that the App Router server and client tRPC helpers can call it with full type inference.
Does the generated router include input sanitization?
It includes Zod validation at the boundary: create uses ${entity}CreateSchema (all fields required), update uses ${entity}UpdateSchema (partial + required uuid id), and getById/delete validate { id: z.string().uuid() }. Zod coerces/validates types; for stricter rules (email, min length) tighten the fields by hand.
Why is the import path ../trpc?
The generator emits a relative ../trpc import assuming the router sits one level under your tRPC instance. Real App Router projects vary — change it to your actual path (often @/server/api/trpc). This is a one-line edit per generated file.
Can I make the list procedure require auth?
Yes — it's generated as publicProcedure, but you can change list/getById to protectedProcedure after generating if those reads must be auth-gated. The generator defaults reads to public.
How do I call this from a Server Component vs a Client Component?
Once registered, use your App Router tRPC helpers: await api.<entity>.list(...) in a Server Component, and api.<entity>.list.useQuery(...) in a Client Component. Both infer types from the generated router, so no manual annotations are needed.
What tRPC version is targeted?
tRPC v11, with the router() / *.input().query() / *.input().mutation() pattern and TRPCError from @trpc/server. It's compatible with App Router setups built on the v11 helpers.
What tier and limits apply?
Developer tier only; lower tiers see an upgrade overlay. The Developer per-file limit is 500 MB with unlimited rows, and it's a single-workbook tool (multi-sheet workbooks generate sub-routers automatically).
Does my schema spreadsheet leave my machine?
No. SheetJS parses it in the browser; field names and sample data never reach a server. Only an anonymous processed-file counter is stored for dashboard stats.
Can I generate several entities for one App Router API at once?
Yes — use a multi-tab .xlsx (one entity per sheet). The tool produces a root router mounting one sub-router per sheet, which you can mount whole on appRouter or splice into your existing root.
Is the output deterministic enough to commit and diff?
Yes — the same sheet always yields identical code. You can commit generated routers and re-run the tool when the schema changes to see a clean diff of added/removed fields. For other artifacts from the same schema, json-to-typescript emits standalone interfaces and excel-tailwind-export renders the data as a styled HTML table.
Privacy first
Every JAD Excel tool runs entirely in your browser using SheetJS and ExcelJS. Your spreadsheets, formulas, and data never leave your device — verified by zero outbound network requests during processing.