dotmd

Next.js Full-Stack — Windsurf Rules

Windsurf rules for full-stack Next.js work using end-to-end Cascade workflows.

By dotmd TeamCC0Published Feb 19, 2026View source ↗

Install path

Use this file for each supported tool in your project.

  • Windsurf: Save as .windsurfrules in your project at .windsurfrules.

Configuration

.windsurfrules

1# Next.js Full-Stack — Windsurf Rules
2
3Full-stack Next.js App Router application. Server Components by default. Server Actions for mutations. Cascade: think in end-to-end flows — a feature touches DB schema, server logic, validation, and UI. Wire them together.
4
5## Quick Reference
6
7| Area | Convention |
8|---|---|
9| Framework | Next.js 14+ (App Router only) |
10| Language | TypeScript, strict mode |
11| Styling | Tailwind CSS + `cn()` utility |
12| Package manager | pnpm |
13| Components | Server Components default; `"use client"` pushed to leaf nodes |
14| Data fetching | Server Components + `fetch` with caching / `unstable_cache` |
15| Mutations | Server Actions with Zod validation |
16| Database | Prisma (or Drizzle — check `package.json`) |
17| Auth | NextAuth / Auth.js or Clerk (check existing setup) |
18| Validation | Zod — shared schemas between client and server |
19| Testing | Vitest + React Testing Library |
20| Forms | `useActionState` + Server Actions (or React Hook Form if present) |
21
22## Project Structure
23
24```
25├── app/
26│ ├── layout.tsx # Root layout (Server Component)
27│ ├── page.tsx # Home page
28│ ├── globals.css # Tailwind directives
29│ ├── (marketing)/ # Route group — public pages
30│ │ ├── page.tsx
31│ │ └── pricing/page.tsx
32│ ├── (app)/ # Route group — authenticated
33│ │ ├── layout.tsx # Auth guard layout
34│ │ ├── dashboard/page.tsx
35│ │ └── settings/page.tsx
36│ └── api/ # Route Handlers (webhooks, OAuth callbacks only)
37│ └── webhooks/stripe/route.ts
38├── components/
39│ ├── ui/ # Primitives (button, input, card, dialog)
40│ └── [feature]/ # Feature-scoped (invoice-table, user-form)
41├── lib/
42│ ├── utils.ts # cn() helper
43│ ├── db.ts # Prisma/Drizzle client (server-only)
44│ ├── auth.ts # Auth helpers (server-only)
45│ ├── validations/ # Zod schemas (shared)
46│ └── actions/ # Server Actions grouped by domain
47├── hooks/ # Client-side React hooks
48├── types/ # Shared TypeScript types
49├── prisma/
50│ └── schema.prisma # Database schema
51└── tailwind.config.ts
52```
53
54## Full-Stack Flow Pattern
55
56When Cascade implements a feature, it touches multiple layers. Here's the expected flow:
57
58### 1. Schema → 2. Validation → 3. Server Action → 4. UI
59
60**Example: Creating an invoice**
61
62**Step 1: Database schema** (if the model doesn't exist)
63
64```prisma
65model Invoice {
66 id String @id @default(cuid())
67 number String @unique
68 clientId String
69 client Client @relation(fields: [clientId], references: [id])
70 items InvoiceItem[]
71 status InvoiceStatus @default(DRAFT)
72 total Decimal @db.Decimal(10, 2)
73 dueDate DateTime
74 createdAt DateTime @default(now())
75 updatedAt DateTime @updatedAt
76}
77
78enum InvoiceStatus {
79 DRAFT
80 SENT
81 PAID
82 OVERDUE
83 CANCELLED
84}
85```
86
87After schema changes, run: `pnpm prisma migrate dev --name <description>` then `pnpm prisma generate`.
88
89**Step 2: Validation schema** (`lib/validations/invoice.ts`)
90
91```typescript
92import { z } from "zod"
93
94export const invoiceItemSchema = z.object({
95 description: z.string().min(1, "Description is required"),
96 quantity: z.number().int().positive(),
97 unitPrice: z.number().positive(),
98})
99
100export const createInvoiceSchema = z.object({
101 clientId: z.string().min(1, "Client is required"),
102 items: z.array(invoiceItemSchema).min(1, "At least one line item required"),
103 dueDate: z.coerce.date().refine(
104 (date) => date > new Date(),
105 "Due date must be in the future"
106 ),
107 notes: z.string().max(500).optional(),
108})
109
110export type CreateInvoiceInput = z.infer<typeof createInvoiceSchema>
111```
112
113**Step 3: Server Action** (`lib/actions/invoice.ts`)
114
115```typescript
116"use server"
117
118import { z } from "zod"
119import { revalidatePath } from "next/cache"
120import { db } from "@/lib/db"
121import { getCurrentUser } from "@/lib/auth"
122import { createInvoiceSchema } from "@/lib/validations/invoice"
123
124type ActionResult =
125 | { success: true; invoiceId: string }
126 | { success: false; error: string; fieldErrors?: Record<string, string[]> }
127
128export async function createInvoice(formData: FormData): Promise<ActionResult> {
129 const user = await getCurrentUser()
130 if (!user) {
131 return { success: false, error: "Unauthorized" }
132 }
133
134 const raw = {
135 clientId: formData.get("clientId"),
136 items: JSON.parse(formData.get("items") as string),
137 dueDate: formData.get("dueDate"),
138 notes: formData.get("notes"),
139 }
140
141 const parsed = createInvoiceSchema.safeParse(raw)
142 if (!parsed.success) {
143 return {
144 success: false,
145 error: "Validation failed",
146 fieldErrors: parsed.error.flatten().fieldErrors,
147 }
148 }
149
150 const { items, ...invoiceData } = parsed.data
151 const total = items.reduce(
152 (sum, item) => sum + item.quantity * item.unitPrice, 0
153 )
154
155 const invoice = await db.invoice.create({
156 data: {
157 ...invoiceData,
158 number: await generateInvoiceNumber(),
159 total,
160 items: { create: items },
161 userId: user.id,
162 },
163 })
164
165 revalidatePath("/invoices")
166 return { success: true, invoiceId: invoice.id }
167}
168```
169
170**Step 4: UI component** (`components/invoice/create-invoice-form.tsx`)
171
172```tsx
173"use client"
174
175import { useActionState } from "react"
176import { createInvoice } from "@/lib/actions/invoice"
177import { Button } from "@/components/ui/button"
178
179export function CreateInvoiceForm({ clients }: { clients: Client[] }) {
180 const [state, formAction, isPending] = useActionState(createInvoice, null)
181
182 return (
183 <form action={formAction} className="space-y-6">
184 {state?.success === false && (
185 <div className="rounded-md bg-red-50 p-3 text-sm text-red-700">
186 {state.error}
187 </div>
188 )}
189
190 <select name="clientId" required className="w-full rounded-md border p-2">
191 <option value="">Select client</option>
192 {clients.map((c) => (
193 <option key={c.id} value={c.id}>{c.name}</option>
194 ))}
195 </select>
196
197 {/* Line items, due date, etc. */}
198
199 <Button type="submit" disabled={isPending}>
200 {isPending ? "Creating..." : "Create Invoice"}
201 </Button>
202 </form>
203 )
204}
205```
206
207**Step 5: Page wiring** (`app/(app)/invoices/new/page.tsx`)
208
209```tsx
210import { db } from "@/lib/db"
211import { CreateInvoiceForm } from "@/components/invoice/create-invoice-form"
212
213export default async function NewInvoicePage() {
214 const clients = await db.client.findMany({
215 orderBy: { name: "asc" },
216 select: { id: true, name: true },
217 })
218
219 return (
220 <div className="mx-auto max-w-2xl py-8">
221 <h1 className="text-2xl font-bold">New Invoice</h1>
222 <CreateInvoiceForm clients={clients} />
223 </div>
224 )
225}
226```
227
228## Server Components vs Client Components
229
230**Server Component (default — no directive):**
231- Fetches data directly (DB, APIs)
232- Has access to server-only modules (`db`, `auth`, env vars)
233- Cannot use hooks, event handlers, or browser APIs
234
235**Client Component (`"use client"`):**
236- Handles interactivity (forms, state, effects)
237- Push `"use client"` as LOW as possible — wrap only the interactive piece
238
239```tsx
240// ✅ Server Component fetches → passes to tiny client island
241import { db } from "@/lib/db"
242import { LikeButton } from "./like-button" // "use client" inside
243
244export async function PostCard({ id }: { id: string }) {
245 const post = await db.post.findUnique({ where: { id } })
246 if (!post) return null
247
248 return (
249 <article className="rounded-lg border p-4">
250 <h2 className="font-semibold">{post.title}</h2>
251 <p className="text-zinc-600">{post.excerpt}</p>
252 <LikeButton postId={id} initialCount={post.likes} />
253 </article>
254 )
255}
256```
257
258## Data Fetching & Caching
259
260```typescript
261// ✅ Cached database query with tag-based revalidation
262import { unstable_cache } from "next/cache"
263
264const getInvoices = unstable_cache(
265 async (userId: string) => {
266 return db.invoice.findMany({
267 where: { userId },
268 orderBy: { createdAt: "desc" },
269 include: { client: { select: { name: true } } },
270 })
271 },
272 ["invoices"],
273 { tags: ["invoices"], revalidate: 60 }
274)
275
276// In Server Action after mutation:
277revalidateTag("invoices")
278```
279
280**Rules:**
281- Fetch in Server Components. Pass data down as props.
282- Use `loading.tsx` and `<Suspense>` for streaming.
283- Route Handlers (`app/api/`) are for webhooks and OAuth callbacks — not internal data fetching.
284
285## Database Patterns
286
287### Prisma Conventions
288
289- Keep `db` instance in `lib/db.ts` — import as `import { db } from "@/lib/db"`.
290- Use `select` to fetch only needed fields. Avoid `include` with large relations.
291- Wrap multi-step operations in `db.$transaction()`.
292- Optimistic updates: check `updatedAt` or version field before writing.
293
294```typescript
295// ✅ Transaction for multi-step mutation
296async function markInvoicePaid(invoiceId: string, paymentId: string) {
297 return db.$transaction(async (tx) => {
298 const invoice = await tx.invoice.update({
299 where: { id: invoiceId, status: "SENT" },
300 data: { status: "PAID" },
301 })
302 await tx.payment.create({
303 data: { invoiceId, externalId: paymentId, amount: invoice.total },
304 })
305 return invoice
306 })
307}
308```
309
310## Error Handling
311
312- **Server Actions:** return `{ success: false, error: "..." }` — never throw.
313- **Server Components:** use `error.tsx` boundaries and `notFound()`.
314- **Client Components:** use Error Boundaries or try/catch in event handlers.
315
316```tsx
317// app/(app)/invoices/[id]/error.tsx
318"use client"
319
320export default function InvoiceError({
321 error,
322 reset,
323}: {
324 error: Error
325 reset: () => void
326}) {
327 return (
328 <div className="rounded-md bg-red-50 p-6 text-center">
329 <h2 className="text-lg font-semibold text-red-800">Something went wrong</h2>
330 <p className="mt-2 text-sm text-red-600">{error.message}</p>
331 <button onClick={reset} className="mt-4 rounded bg-red-600 px-4 py-2 text-white">
332 Try again
333 </button>
334 </div>
335 )
336}
337```
338
339## Tailwind Conventions
340
341- Use `cn()` from `lib/utils.ts` for conditional classes.
342- Mobile-first responsive: `sm:`, `md:`, `lg:`.
343- Always provide `dark:` variants for colors and borders.
344- No arbitrary values (`w-[347px]`) — extend the theme in `tailwind.config.ts`.
345- Component variants via `cn()` with conditional objects — not ternary strings.
346
347## Auth Guard Pattern
348
349```tsx
350// app/(app)/layout.tsx — protects all (app) routes
351import { redirect } from "next/navigation"
352import { getCurrentUser } from "@/lib/auth"
353
354export default async function AppLayout({ children }: { children: React.ReactNode }) {
355 const user = await getCurrentUser()
356 if (!user) redirect("/login")
357
358 return <>{children}</>
359}
360```
361
362## File & Naming Conventions
363
364| What | Convention |
365|---|---|
366| Files | kebab-case: `create-invoice-form.tsx`, `auth-utils.ts` |
367| Components | PascalCase exports: `CreateInvoiceForm` |
368| Server Actions | camelCase functions: `createInvoice`, `updateInvoiceStatus` |
369| Zod schemas | camelCase with Schema suffix: `createInvoiceSchema` |
370| Route files | Next.js conventions: `page.tsx`, `layout.tsx`, `loading.tsx`, `error.tsx` |
371| Barrel exports | Avoid — import from source module directly |
372
373## Cascade Flow Guidance
374
375When implementing a feature end-to-end:
376
3771. **Start with the data model.** Does the schema support this feature? Add/modify Prisma models first.
3782. **Define validation.** Write the Zod schema for input validation. This is the contract between client and server.
3793. **Implement the server action or data query.** This is the business logic layer.
3804. **Build the UI last.** Server Component page → Client Component for interactivity.
3815. **Add `loading.tsx`** for any page that fetches data.
3826. **Revalidate caches** after mutations with `revalidatePath` or `revalidateTag`.
383
384When modifying an existing feature, trace the full flow before editing: schema → validation → action → UI. A change in one layer usually requires changes in others.
385
386## Commands
387
388```bash
389pnpm dev # Dev server with hot reload
390pnpm build # Production build (type-checks)
391pnpm lint # ESLint
392pnpm test # Vitest
393pnpm prisma studio # Visual DB browser
394pnpm prisma migrate dev --name x # Create + apply migration
395pnpm prisma generate # Regenerate Prisma client
396pnpm prisma db seed # Run seed script
397```
398

Community feedback

0 found this helpful

Works with: