How to turn an excel requirements sheet into a working trpc api prototype
- Step 1Sketch the entity in a sheet — One header row of field names is enough. Add 3–5 example rows that reflect the real value shapes (numbers in numeric fields,
true/falsein flags,YYYY-MM-DDin dates) so the inferred Zod types are useful for the prototype. - Step 2Open the tool on a Developer account — It's Developer-tier; lower tiers see an upgrade overlay and the error
tRPC Router Builder requires Developer tier.Drop the.xlsxor.csv— parsing is local. - Step 3Name the router — Type something like
eventsRouter. The entity (Event) is derived by stripping the trailingrouterand PascalCasing — no separate entity field to fill in. - Step 4Generate and skim the preview — The code preview shows the first 4,000 characters. Check the procedure set and inferred types at a glance; the structure is ready to paste even before you replace the data layer.
- Step 5Paste into your prototype — Copy or download
<routerName>.tsintoserver/api/routers/, register it on your rootappRouter, and you have type-safe endpoints. Stub thectx.dbplaceholders with an in-memory array first if you just want to demo. - Step 6Regenerate as requirements shift — When the spreadsheet gains or loses a column, re-run the tool to refresh the router. Because generation is deterministic, you can diff the new file against the old to see exactly what changed.
What you get per generate (prototype-ready)
Everything below comes out of a single-sheet drop. The list procedure is the one most prototypes lean on first.
| Procedure | Access | Prototype use |
|---|---|---|
list | publicProcedure | Paginated table view with built-in search over the first 2 text columns |
getById | publicProcedure | Detail page; throws TRPCError NOT_FOUND for a bad id |
create | protectedProcedure | New-record form, validated by ${entity}CreateSchema |
update | protectedProcedure | Edit form, validated by ${entity}UpdateSchema |
delete | protectedProcedure | Row delete; returns { success: true } |
list procedure input (pagination + search)
The generated list input is the same for every entity — only the searched columns differ.
| Input field | Zod | Default / bound |
|---|---|---|
page | z.number().int().min(1) | default 1 |
pageSize | z.number().int().min(1).max(100) | default 20, hard cap 100 |
search | z.string().optional() | filters first 2 string columns, contains + insensitive |
Speed and limits
Developer-tier numbers; the tool is single-file and runs locally.
| Property | Value |
|---|---|
| Time to scaffold | One file-drop + a router name |
| Input formats | .xlsx, .xls, .ods, .csv |
| Output | One .ts file, named <routerName>.ts |
| Determinism | Same sheet → identical output (no AI / randomness) |
| Required tier | Developer |
| Developer file / rows | 500 MB / unlimited |
| Multi-entity prototype | Multi-tab workbook → one sub-router per tab |
Cookbook
Fast paths from a throwaway requirements sheet to a demoable tRPC endpoint.
Events entity to a list-and-detail prototype
An Events sheet generates a router whose list (paginated + searchable) and getById back a table-plus-detail UI immediately. Stub ctx.db with an array to demo before the DB exists.
Input (sheet: Events):
id,title,startDate,capacity,published
1,Launch Party,2026-07-01,200,true
2,Webinar,2026-07-15,500,false
Router name: eventsRouter
Generated eventsRouter.ts gives you:
list: publicProcedure (page/pageSize/search) -> ctx.db.event.findMany
getById: publicProcedure ({ id: uuid }) -> ctx.db.event.findUnique
create/update/delete: protectedProcedure
Inferred types:
id z.number(), title z.string(), startDate z.string() (date->string),
capacity z.number(), published z.boolean()Demo with an in-memory stub before the DB
For a quick prototype, replace the Prisma placeholder with an array so the endpoints work without any database wiring.
// Generated: const data = await ctx.db.event.findMany({ ... });
// Quick prototype stub:
const MEM: Event[] = [{ id: 1, title: "Launch Party", capacity: 200, published: true }];
list: publicProcedure
.input(/* same generated input */)
.query(({ input }) => {
const start = (input.page - 1) * input.pageSize;
return { data: MEM.slice(start, start + input.pageSize), page: input.page, pageSize: input.pageSize };
}),Searchable columns are the first two strings
Search in the prototype table works on the first two z.string() columns automatically. Order your headers so the columns you want to search come first.
Headers order matters for search:
id, title, location, description
title -> 1st string (searched)
location -> 2nd string (searched)
description -> 3rd string (NOT searched by default)
To search description too, add it to the generated OR array:
{ description: { contains: search, mode: "insensitive" } }Iterate: add a column, regenerate, diff
Add an organizer column to the sheet and regenerate. Deterministic output means a clean diff shows exactly the new field.
v1 headers: id, title, capacity
v2 headers: id, title, capacity, organizer
Diff of generated schema:
ProductSchema = z.object({
id: z.number().optional(),
title: z.string().optional(),
capacity: z.number().optional(),
+ organizer: z.string().optional(),
});
// Same input -> same output, so version control shows just the delta.Multi-screen prototype from one workbook
Tabs for Users, Events, and Tickets generate a root router with three sub-routers in a single pass — enough to back a three-screen prototype.
Workbook tabs: Users | Events | Tickets
Router name: apiRouter
export const apiRouter = router({
users: usersRouter,
events: eventsRouter,
tickets: ticketsRouter,
});
export type ApiRouter = typeof apiRouter;
// Mount apiRouter on your root appRouter and all three screens have endpoints.Edge cases and what actually happens
Tool requires Developer tier
Developer requiredPrototyping on a Free / Pro / Pro+Media account fails — the tool throws tRPC Router Builder requires Developer tier. and shows an upgrade overlay. Use a Developer-tier account (or runner key) for the generate.
Date column comes out as z.string()
By designISO YYYY-MM-DD columns are typed z.string(), not z.date(). For a prototype this is usually fine, but if a form binds to a date picker you may want z.coerce.date() — change those fields after generating.
All fields optional means loose validation
ExpectedThe base schema makes everything .optional(), so reads accept partial records. create re-requires all fields via .required(). For a quick prototype you may want to relax create for fields you haven't built inputs for yet.
Search misses a column you expected
By designOnly the first two z.string() columns are wired into search. If your demo's search box returns nothing for a third text field, add it to the generated OR array manually.
Numeric id vs uuid mismatch in demo
Manual fixgetById expects z.string().uuid() but a spreadsheet id is usually numeric. In a stub-backed prototype, either pass UUID strings as ids or change the getById/delete input to z.number() to match your in-memory data.
First row is data, not headers
Headers from row 1SheetJS uses row 1 as field names. A throwaway sheet that starts with data will produce fields named after those first values. Keep a header row at the top even for scratch sheets.
Default Sheet1 tab name leaks into entity name
Multi-sheet behaviourIn multi-tab mode the entity name comes from the sheet name. Leaving tabs as Sheet1 / Sheet2 produces Sheet1 / Sheet2 entities. Rename tabs to the entities you're prototyping.
Mixed-type column silently becomes string
z.string() fallbackA column mixing numbers and the odd text value (42, TBD) can't be all-numeric, so it falls back to z.string(). Clean throwaway data, or accept the looser type for the prototype.
Output preview truncated at 4,000 chars
Preview onlyLarge multi-entity outputs are cut off in the on-page preview. The complete file is available via Copy (full string) or Download — don't paste from a truncated preview.
Empty sheet returns no router
Empty outputA sheet with headers but zero data rows still works (types fall back to string), but a workbook with no data rows at all returns // No data. Add a sample row so inference has something to read.
Frequently asked questions
How fast is this really?
One file-drop plus typing a router name. The generator is deterministic SheetJS + templates, so there's no model latency — you get a complete, valid tRPC v11 router (schema, interface, five procedures) immediately, ready to paste into server/api/routers/.
Does the generated code pass TypeScript strict mode?
The output uses explicit types throughout and is written to compile under strict mode; optional fields use ? from Zod .optional(). You'll still need a Prisma-shaped (or stubbed) ctx.db so the placeholder calls type-check — swap them for an in-memory array to demo without a database.
Can I generate routers for multiple entities at once?
Yes — put one entity per tab in a multi-sheet workbook and the tool emits a root router with one sub-router per tab. For separate single-entity files, run the tool once per sheet. Each run produces one .ts file.
Does the list endpoint support pagination out of the box?
Yes. The generated list input includes page (int ≥1, default 1) and pageSize (int 1–100, default 20), plus an optional search. The placeholder query already applies skip/take, so a paginated table view works as soon as you back ctx.db.
Will the output change if I run it twice on the same sheet?
No — it's deterministic. The same headers and sample data always produce identical code (no AI, no randomness). That makes it safe to regenerate and diff against version control as your requirements evolve.
What's the one option?
Router name (default dataRouter). It names the output file and derives the single-sheet entity name. There's no type-override or template-style option — keep your sample data clean to steer the inferred types.
Why is my id field validated as a UUID?
getById, delete, and the update key are hard-coded to z.string().uuid() regardless of the inferred id type. For a numeric-id prototype, change them to z.number() (or pass UUID strings in your stub data) so the endpoints accept your ids.
What tier do I need?
Developer. On lower tiers the tool throws tRPC Router Builder requires Developer tier. and shows an upgrade prompt. Developer also raises the per-file limit to 500 MB with unlimited rows.
Can I paste data straight into a sheet instead of exporting?
Yes — any .xlsx / .csv with a header row and a few sample rows works, including a scratch sheet you typed by hand. SheetJS parses it locally in your browser; nothing is uploaded. If your headers are messy (spaces, casing), tidy them first with csv-header-rename so the inferred Zod field names come out clean.
How do I stub the data layer for a demo?
Replace each ctx.db.<entity>.* placeholder with an in-memory array (e.g. MEM.slice(...) for list, MEM.find(...) for getById). The Zod validation and tRPC type-safety still work, so your prototype UI gets full type inference without a database.
Which tRPC version is generated?
tRPC v11, importing from ../trpc and @trpc/server. Adjust the relative ../trpc import to match where your prototype defines its tRPC instance.
Does my throwaway data leave my browser?
No. Parsing happens in-browser via SheetJS. Even scratch sample data stays local; only an anonymous run counter is recorded for dashboard stats.
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.