Fullstack Wiring
Connect the frontend, API, and database so a button click round-trips to the DB and back.
Why
The 'fullstack wiring' is the moment a product becomes real — a user clicks, data flows to the DB, and a result appears on screen. Before this, you have a pretty UI mockup and a DB schema, but no product. The wiring is where most bugs live: the frontend sends the wrong field name, the API doesn't validate, the ORM silently drops a column, and the user sees a generic error. A disciplined wiring uses end-to-end type safety (frontend types generated from the API schema, API types from the DB schema), so a rename is a compile error across the whole stack. Without it, the three layers drift and every integration is a debugging session.
When
After the UI and DB schema exist, before the first user feature. Takes 2-4 h. Skip only for pure static sites.
How
1. Generate API types from the server and import them in the client
- tRPC: types flow automatically from server router to client
- REST: openapi-typescript generates types from the OpenAPI spec
- Never hand-write client types that duplicate the server's
2. Use an ORM with typed queries (Prisma, Drizzle) so DB results are typed
- prisma.user.findUnique returns a typed User, not any
- Schema changes → prisma generate → type errors in every broken query
3. Wire a single feature end-to-end first as the template
- Button → fetch /api/items → prisma.item.create → return typed Item → render
- This becomes the copy-paste pattern for every future feature
4. Add a server action or route handler that validates input with zod
- const schema = z.object({ name: z.string().min(1) })
- Parse before touching the DB; return typed errors the UI can show
5. Test the round-trip: a Playwright test that clicks the button and asserts the DB row exists
- This is the only test that catches wiring bugs across all three layersExample
// Server: app/api/items/route.ts (Next.js + Prisma + zod)
import { prisma } from "@/lib/prisma";
import { z } from "zod";
const schema = z.object({ name: z.string().min(1), qty: z.coerce.number().int().positive() });
export async function POST(req: Request) {
const input = schema.parse(await req.json()); // throws on invalid
const item = await prisma.item.create({ data: input }); // typed Item
return Response.json(item); // typed response
}
// Client: types flow from the server
import type { Item } from "@prisma/client"; // single source of truth
async function createItem(input: { name: string; qty: number }): Promise<Item> {
const res = await fetch("/api/items", { method: "POST", body: JSON.stringify(input) });
return res.json();
}Common Problems
- ⚠Hand-writing client types that drift from the server, so a field rename breaks the client silently.
- ⚠Skipping input validation on the server because 'the form validates it', so a direct API call bypasses all checks.
- ⚠Using prisma.$queryRaw with template strings, losing type safety and opening SQL injection.
- ⚠No end-to-end test, so a wiring bug (wrong field name, missing column) only surfaces in manual QA.
Best Practices
- ✓Use tRPC or a generated client so types flow from server to client with zero manual sync.
- ✓Validate on the server with zod, and infer the client type from the zod schema (z.infer<typeof schema>).
- ✓Keep one feature end-to-end as the 'golden path' that every new feature copies — consistency over novelty.
- ✓Write the Playwright e2e test before the feature is 'done' so wiring bugs surface immediately.