How to generate trpc, zod, and prisma-ready boilerplate from an excel data model
- Step 1Lay out one entity per sheet — Put each model entity on its own tab, with the first row as field names matching your intended Prisma fields. Multi-tab workbooks generate one sub-router per sheet, with the entity name taken from the sheet name — so name tabs
User,Product,Order, notSheet1. - Step 2Keep representative sample rows — Type inference reads the first 50 non-empty values per column. Make sure ID columns hold numbers, flag columns hold true/false, and date columns use
YYYY-MM-DDso the inferred Zod types land where you expect. - Step 3Upload on a Developer-tier account — The tool is Developer-only; lower tiers get an upgrade overlay and the error
tRPC Router Builder requires Developer tier.Drop the.xlsx/.csv; SheetJS parses it locally. - Step 4Name the router — Enter a camelCase router name (
productsRouter). For a single sheet the entity is derived from it (Product); for multiple sheets the name becomes the root router and each sheet contributes a sub-router. - Step 5Review inferred types against your Prisma model — Check that numeric fields are
z.number(), booleansz.boolean(), and that date fields (left asz.string()) match how you store them in Prisma. Tighten anything that should bez.string().email(),z.string().datetime(), or an enum. - Step 6Drop into server/api/routers/ and wire up Prisma — Download
<routerName>.ts, place it in your routers directory, register it on the rootappRouter, and swap eachctx.db.<entity>.*placeholder so it matches the real model name and types in yourschema.prisma.
Spreadsheet model to TypeScript layer
What the tool generates versus what you still own. The split matters: it produces the tRPC/Zod layer, not the Prisma datamodel.
| Artifact | Generated by this tool? | Notes |
|---|---|---|
${Entity}Schema (Zod z.object) | Yes | All fields .optional(); types inferred from data |
export interface ${Entity} | Yes | Mirrors the Zod fields (? optional) |
${entity}CreateSchema / ${entity}UpdateSchema | Yes | .required() and .partial() + id: z.string().uuid() |
| tRPC router with CRUD procedures | Yes | list/getById public, create/update/delete protected |
ctx.db.<entity>.* data calls | Placeholders only | Prisma-shaped; replace with your real queries |
schema.prisma model / migration | No | You author the Prisma datamodel separately |
Root appRouter registration | No (you wire it) | Multi-sheet root router still needs mounting on your app root |
Type inference rules
Applied per column over the first 50 non-empty samples; first matching rule wins.
| Column data | Zod field | Prisma type you'd typically pair it with |
|---|---|---|
All numeric (1, 2, 19.99) | z.number() | Int / Float / Decimal |
All YYYY-MM-DD | z.string() (not z.date()) | DateTime — coerce in your resolver or tighten the Zod field |
All true / false | z.boolean() | Boolean |
| Mixed / text | z.string() | String |
| All blank | z.string() | depends on the column's real meaning |
Inputs, output, and tier limits
Developer-only tool; single workbook in, one TypeScript file out.
| Property | Value |
|---|---|
| Accepted input | .xlsx, .xls, .ods, .csv |
| Output | One .ts file (<routerName>.ts) — tRPC + Zod, Prisma placeholders |
| Prisma schema output | None — the tool does not write schema.prisma |
| Options | routerName only (default dataRouter) |
| Required tier | Developer |
| Developer file size / rows | 500 MB / unlimited rows |
| Multi-entity | Multiple sheets → nested router, one sub-router per sheet |
Cookbook
Going from a spreadsheet data model to a Prisma-ready tRPC layer, with the manual edits called out.
Single-entity model to a Prisma-ready router
A Product tab with id, name, price, inStock. The generator types price as z.number() and inStock as z.boolean(); you then map ctx.db.product to your actual Prisma model.
Input (sheet: Product):
id,name,price,inStock
1,Widget,19.99,true
2,Gadget,4.50,false
Router name: productsRouter
Generated (excerpt):
export const ProductSchema = z.object({
id: z.number().optional(),
name: z.string().optional(),
price: z.number().optional(),
inStock: z.boolean().optional(),
});
export const productCreateSchema = ProductSchema.required();
// create procedure:
create: protectedProcedure
.input(productCreateSchema)
.mutation(async ({ input, ctx }) => ctx.db.product.create({ data: input })),
// Manual step: confirm ctx.db.product matches your prisma model name.Multi-tab model becomes a nested router
A workbook with User, Product, and Order tabs generates a root router mounting three sub-routers. Each entity name comes from its sheet name, so the tab labels are load-bearing.
Workbook tabs: User | Product | Order
Router name: appRouter
Generated appRouter.ts:
const userRouter = router({ /* CRUD for User */ });
const productRouter = router({ /* CRUD for Product */ });
const orderRouter = router({ /* CRUD for Order */ });
export const appRouter = router({
user: userRouter,
product: productRouter,
order: orderRouter,
});
export type AppRouter = typeof appRouter;Reconciling the id type with Prisma
Spreadsheets store ids as plain integers so the tool infers z.number(), but getById/update/delete hard-code z.string().uuid(). Pick the truth that matches your Prisma @id and edit both places.
Inferred: id: z.number().optional()
Fixed by generator: getById input -> z.object({ id: z.string().uuid() })
If your Prisma model uses Int @id @default(autoincrement()):
- change getById/delete input to z.object({ id: z.number().int() })
- change updateSchema's id to z.number().int()
If it uses String @id @default(cuid()):
- change z.string().uuid() to z.string() (cuid isn't a uuid)Dates need coercion before they hit Prisma
A signupDate column stays z.string(). Prisma DateTime fields expect a Date, so either coerce in the resolver or tighten the Zod field after generating.
Inferred: signupDate: z.string().optional() // ISO date kept as string
Option A (tighten the schema):
signupDate: z.coerce.date().optional()
Option B (coerce in resolver before ctx.db.create):
const data = { ...input, signupDate: new Date(input.signupDate!) };Required-on-create vs optional-on-read
The base schema marks every field optional; createSchema re-requires all of them via .required(). If a column is genuinely nullable in your DB, relax it in createSchema by hand.
export const UserSchema = z.object({
id: z.number().optional(),
email: z.string().optional(),
middleName: z.string().optional(), // nullable in DB
});
export const userCreateSchema = UserSchema.required(); // forces middleName!
// Fix for a nullable column:
export const userCreateSchema = UserSchema.required().partial({ middleName: true });Edge cases and what actually happens
Expecting a schema.prisma file
Not generatedThe tool produces only the tRPC + Zod layer. There is no .prisma output — you author and migrate the Prisma datamodel yourself. The generated ctx.db.<entity>.* calls assume a Prisma-shaped context but won't create or validate against your actual models.
ctx.db entity name vs your Prisma model name
Manual mappingThe generator names the data accessor after the camelCased entity (ctx.db.product). If your Prisma model is named differently (e.g. Products mapping to ctx.db.products), the placeholder won't resolve. Align the accessor with your real model name after generating.
Developer tier required
Developer requiredOn Free / Pro / Pro+Media the processor throws tRPC Router Builder requires Developer tier. and the UI shows an upgrade overlay. Full-stack teams scripting this should ensure the account or runner key is on the Developer plan.
Date columns generated as z.string()
By designISO YYYY-MM-DD columns are kept as z.string(). Prisma DateTime fields need a Date — coerce in your resolver or change the Zod field to z.coerce.date() before wiring the create/update mutations to ctx.db.
All fields optional in the base schema
ExpectedEvery field in ${Entity}Schema is .optional(). create re-requires all of them via .required(). If your model has genuinely nullable columns, relax them in ${entity}CreateSchema (e.g. with .partial({ field: true })) so create doesn't reject valid null inputs.
Numeric id vs hard-coded UUID validation
Manual fixgetById, delete, and the update schema's id are all z.string().uuid() regardless of the inferred id type. For an Int @id @default(autoincrement()) Prisma model, swap them to z.number().int(); for a CUID model use z.string() (a cuid is not a uuid).
Tab named Sheet1 produces a Sheet1 entity
Multi-sheet behaviourSub-router entity names come from sheet names, not the router name. A default Sheet1 tab yields a Sheet1 entity and ctx.db.sheet1. Rename tabs to your intended entity names before generating a multi-entity router.
Search only covers two string fields
By designThe list procedure's search filter ORs over only the first two z.string() columns with contains + insensitive mode. For models where another text field should be searchable, extend the generated where clause manually.
Headers not in the first row
Headers from row 1SheetJS treats row 1 as headers. A metadata/title row above your field names will be parsed as the field list. Strip any preamble so the first row contains only the model's field names.
Empty workbook
Empty outputIf no sheet contains data rows, the tool returns // No data instead of a router. Ensure each entity tab has a header row plus at least one sample row before generating.
Frequently asked questions
Does it generate a Prisma schema (.prisma) too?
No. The tool generates the tRPC router plus Zod schemas and a TypeScript interface, with Prisma-style ctx.db placeholders. It does not emit a schema.prisma datamodel — you author Prisma models and migrations separately and then point the placeholders at them.
Can I use this with Drizzle ORM instead of Prisma?
Yes. The Zod schemas and router structure are ORM-agnostic; only the ctx.db.<entity>.findMany / create / ... placeholders are Prisma-shaped. Replace them with Drizzle select() / insert() / update() / delete() calls. Everything else stays the same.
How do I model multiple related entities at once?
Put one entity per sheet/tab in the same workbook. The tool then generates a root router that mounts one sub-router per sheet, with each entity named after its sheet. You still wire foreign-key relations yourself in your Prisma schema and resolvers.
Why are my date fields z.string() and not z.date()?
By design — ISO YYYY-MM-DD columns are kept as z.string(). For Prisma DateTime fields, coerce the string to a Date in your resolver or change the Zod field to z.coerce.date() after generating.
Will the create schema match my nullable columns?
Not automatically. create uses ${Entity}Schema.required(), forcing every field. For nullable DB columns, relax them in the create schema by hand (for example with .partial({ middleName: true })) so valid inserts aren't rejected.
What type is the id field?
In the schema/interface, id is typed from your data (usually z.number() because spreadsheets store integer ids). But getById, delete, and the update key are hard-coded to z.string().uuid(). Reconcile these with your real Prisma @id type after generating.
What's the only option I can set?
Router name (default dataRouter). It names the output file and, for single-sheet input, derives the entity name. There are no type-override, naming-convention, or Prisma-mapping options — those are manual edits.
What tier do I need?
Developer. On lower tiers the tool throws tRPC Router Builder requires Developer tier. and shows an upgrade overlay. Developer also gives a 500 MB file ceiling and unlimited rows, which matters for large seed/data-model exports.
Does my data model leave my machine?
No. SheetJS parses the workbook in your browser; field names and sample rows never reach a server. Only an anonymous processed-file counter is recorded for dashboard stats.
Which tRPC version and import paths are used?
tRPC v11, importing { router, publicProcedure, protectedProcedure } from ../trpc and TRPCError from @trpc/server. Update the relative ../trpc path to your project's tRPC instance location.
Can it read a CSV export of my data dictionary?
Yes — .csv is accepted alongside .xlsx / .xls / .ods. A single CSV table generates a flat CRUD router; to model several entities you need a multi-sheet .xlsx (CSV is single-table).
How would I automate regeneration in CI?
On Developer tier, GET /api/v1/tools/excel-trpc-router exposes the option schema; pair the @jadapps/runner and POST the workbook plus { routerName } to http://127.0.0.1:9789 so parsing happens locally. Run it whenever the data-model spreadsheet changes to keep the tRPC layer in sync. If you'd rather emit a SQL DDL or a standalone Zod file from the same sheet, see json-to-sql and json-to-zod.
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.