mit einem Klick
add-resource
// Use when adding a new full-stack CRUD resource (e.g. "add a Comments resource"). Walks through Prisma model, Zod schema, tRPC router, and React page in order. Mirrors the patterns already used by Contact/Task/Project.
// Use when adding a new full-stack CRUD resource (e.g. "add a Comments resource"). Walks through Prisma model, Zod schema, tRPC router, and React page in order. Mirrors the patterns already used by Contact/Task/Project.
Use before pushing to a deployed environment (staging or prod). Walks through env vars, migrations, secrets, build, and rollback prep.
Use the first time an agent (or human) opens this repo. Installs deps, prepares the env, validates that the dev environment works, and points the agent at the right files.
Use when starting a real project from this template. Removes the CRM demo (Contact/Task/Project) cleanly so the repo is a blank slate without breaking guardrails.
Use on every commit. The pre-commit hooks run checks in parallel. If any fail, read ALL errors, fix them in one pass, and retry. NEVER use --no-verify.
| name | add-resource |
| description | Use when adding a new full-stack CRUD resource (e.g. "add a Comments resource"). Walks through Prisma model, Zod schema, tRPC router, and React page in order. Mirrors the patterns already used by Contact/Task/Project. |
Add a new full-stack CRUD resource end-to-end. Total time: ~10 minutes for a simple resource.
Reference: The Contact resource is the simplest existing example. Read it before you start:
prisma/schema.prisma — model Contactsrc/shared/schemas/contact.ts — Zod schemassrc/server/routers/contact.ts — tRPC router (list, byId, create, update, delete)src/client/components/ContactForm.tsx — RHF + Zod formsrc/client/pages/ContactsPage.tsx — page with table, dialog, mutationsReplace Widget / widget with your resource name throughout.
prisma/schema.prismamodel Widget {
id Int @id @default(autoincrement())
name String
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
npm run db:migrate # generate + apply migration; name it "add_widget"
src/shared/schemas/widget.tsimport { z } from 'zod';
export const widgetInput = z.object({
name: z.string().min(1).max(200),
notes: z.string().max(2000).optional().nullable(),
});
export type WidgetInput = z.infer<typeof widgetInput>;
export const widgetUpdateInput = widgetInput.partial().extend({
id: z.number().int().positive(),
});
export type WidgetUpdateInput = z.infer<typeof widgetUpdateInput>;
Then export it from src/shared/schemas/index.ts:
export * from './widget.js';
src/server/routers/widget.tsCopy src/server/routers/contact.ts and adapt:
import { TRPCError } from '@trpc/server';
import { z } from 'zod';
import { widgetInput, widgetUpdateInput } from '../../shared/schemas/index.js';
import { publicProcedure, router } from '../trpc.js';
const idInput = z.object({ id: z.number().int().positive() });
export const widgetRouter = router({
list: publicProcedure.query(({ ctx }) =>
ctx.prisma.widget.findMany({ orderBy: { createdAt: 'desc' } }),
),
byId: publicProcedure.input(idInput).query(async ({ ctx, input }) => {
const w = await ctx.prisma.widget.findUnique({ where: { id: input.id } });
if (!w) throw new TRPCError({ code: 'NOT_FOUND', message: 'Widget not found' });
return w;
}),
create: publicProcedure
.input(widgetInput)
.mutation(({ ctx, input }) => ctx.prisma.widget.create({ data: input })),
update: publicProcedure.input(widgetUpdateInput).mutation(({ ctx, input }) => {
const { id, ...data } = input;
return ctx.prisma.widget.update({ where: { id }, data });
}),
delete: publicProcedure.input(idInput).mutation(async ({ ctx, input }) => {
await ctx.prisma.widget.delete({ where: { id: input.id } });
return { ok: true };
}),
});
Wire it into src/server/routers/index.ts:
import { widgetRouter } from './widget.js';
export const appRouter = router({
contact: contactRouter,
task: taskRouter,
project: projectRouter,
widget: widgetRouter, // ← add
});
src/client/components/WidgetForm.tsxCopy ContactForm.tsx and adapt — same pattern, RHF + Zod resolver.
src/client/pages/WidgetsPage.tsxCopy ContactsPage.tsx and adapt. Wire the route in src/client/App.tsx:
<Route path="/widgets" element={<WidgetsPage />} />
And the nav button in the same file.
Add a basic form test (see src/client/__tests__/ContactForm.test.tsx) and/or a router validation test (see src/server/__tests__/contact.router.test.ts).
git add prisma src
git commit -m "feat(widget): add Widget CRUD resource"
If pre-commit complains: read all errors, fix in one pass, retry. See self-correcting-loop.
src/shared/schemas/ + re-exported from index.tssrc/server/routers/index.tstrpc.widget.*App.tsxnpm run typecheck && npm run lint && npm run test:run all pass