The gap between a written spec and working code is where most projects derail. A spec-driven setup closes that gap by making the spec executable — types, data layer, and routes all derive from a single source of truth.
Start with a Schema
Before writing a single component, define your data models. In a Next.js project this means:
- Zod schemas for runtime validation
- TypeScript types inferred from those schemas
- Prisma or Drizzle models that mirror the same shape
When all three come from one definition, a change to the spec propagates everywhere. The compiler catches drift before it reaches production.
Route Design from the Spec
Your routes are your API contract. Define them upfront as a typed map:
export const routes = {
blog: {
list: "/api/blog" as const,
bySlug: (slug: string) => `/api/blog/${slug}` as const,
},
} as const;
Every page and API handler references this map. No magic strings. No guessing what a route returns.
The Data Layer
Use server components for data fetching. Next.js App Router makes this natural — the component asks for data, the server delivers it, and the types match because they share the same schema.
Keep data fetching in dedicated files, not scattered across components. A src/lib/queries.ts file that exports typed fetchers means any page can get its data in one line.
Keeping Routes and Types in Sync
The biggest maintenance win is automated type generation. When your Prisma schema changes, regenerate the types. When your Zod schemas update, the API response types update. CI should fail if a route returns data that doesn't match its declared shape.
Summary
A spec-driven setup is not slower — it's faster over the life of a project. The upfront investment in types and schemas pays back the first time you refactor without breaking anything.