dotmd
// Config Record

>.cursor/rules — Next.js + TypeScript + Tailwind CSS

Cursor project rules (MDC) for Next.js + TypeScript + Tailwind with App Router and server-action-first conventions.

author:
dotmd Team
license:CC0
published:Feb 19, 2026
// Installation

>Add this file to your project repository:

  • Cursor
    .cursor/rules/
// File Content
.cursor/rules (MDC)
1---
2description: Next.js + TypeScript + Tailwind conventions for App Router and server-action-first workflows.
3globs:
4alwaysApply: true
5---
6
7# .cursorrules — Next.js + TypeScript + Tailwind CSS
8
9You are a senior TypeScript developer working in a Next.js App Router project with Tailwind CSS. You write clean, minimal, production-grade code. You do not over-engineer. You do not add dependencies without justification.
10
11## Quick Reference
12
13| Area | Convention |
14| ----------------- | ------------------------------------------------------------- |
15| Package manager | pnpm |
16| Framework | Next.js 14+ (App Router) |
17| Language | TypeScript (strict mode) |
18| Styling | Tailwind CSS + `cn()` utility |
19| Components | Server Components by default; `"use client"` only when needed |
20| State management | React hooks + URL state; no Redux unless already in the repo |
21| Data fetching | Server Components, `fetch` with caching, Server Actions |
22| Forms | Server Actions + `useActionState` (or React Hook Form if present) |
23| Validation | Zod schemas, shared between client and server |
24| Testing | Vitest + React Testing Library |
25| Linting | ESLint + Prettier (follow existing config) |
26| Imports | Use `@/` path alias (mapped to project root or `src/`) |
27
28## Project Structure
29
30```
31├── app/
32│ ├── layout.tsx # Root layout (Server Component)
33│ ├── page.tsx # Home route
34│ ├── globals.css # Tailwind directives + custom CSS
35│ ├── (marketing)/ # Route group — public pages
36│ ├── (app)/ # Route group — authenticated pages
37│ │ ├── dashboard/
38│ │ │ ├── page.tsx
39│ │ │ └── loading.tsx
40│ │ └── layout.tsx
41│ └── api/ # Route handlers (use sparingly)
42├── components/
43│ ├── ui/ # Primitive/reusable UI (buttons, inputs, cards)
44│ └── [feature]/ # Feature-scoped components
45├── lib/
46│ ├── utils.ts # cn() helper and shared utilities
47│ ├── validations/ # Zod schemas
48│ └── [service].ts # Service-layer modules (db, auth, email)
49├── hooks/ # Custom React hooks (client-only)
50├── types/ # Shared TypeScript types/interfaces
51├── public/ # Static assets
52├── tailwind.config.ts
53├── next.config.ts
54└── tsconfig.json
55```
56
57## Tech Stack Assumptions
58
59- **Next.js 14+** with App Router. Do NOT use Pages Router patterns (`getServerSideProps`, `_app.tsx`, etc.).
60- **TypeScript** in strict mode. No `any`. Use `unknown` and narrow types properly.
61- **Tailwind CSS v3+** for all styling. No CSS modules, no styled-components, no inline style objects unless absolutely necessary (e.g., dynamic calc values).
62- **If the repo uses Prisma:** follow existing schema conventions, use server-only imports.
63- **If the repo uses NextAuth/Auth.js:** use the existing auth helper; don't reinvent session handling.
64- **If the repo uses tRPC:** follow existing router patterns; colocate input schemas with procedures.
65
66## Core Conventions
67
68### Server Components First
69
70Every component is a Server Component unless it needs interactivity. When you reach for `"use client"`, ask: can I push this down to a smaller leaf component instead?
71
72```tsx
73// ✅ Server Component — default, no directive needed
74import { db } from "@/lib/db"
75import { LikeButton } from "./like-button" // client island
76
77export async function PostCard({ id }: { id: string }) {
78 const post = await db.post.findUnique({ where: { id } })
79 if (!post) return null
80
81 return (
82 <article className="rounded-lg border border-zinc-200 p-4 dark:border-zinc-800">
83 <h2 className="text-lg font-semibold">{post.title}</h2>
84 <p className="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
85 {post.excerpt}
86 </p>
87 <LikeButton postId={id} initialCount={post.likes} />
88 </article>
89 )
90}
91```
92
93### The `cn()` Helper
94
95Always use `cn()` for conditional/merged class names. It lives in `lib/utils.ts`:
96
97```ts
98import { type ClassValue, clsx } from "clsx"
99import { twMerge } from "tailwind-merge"
100
101export function cn(...inputs: ClassValue[]) {
102 return twMerge(clsx(inputs))
103}
104```
105
106Use it any time you accept `className` as a prop or conditionally apply classes:
107
108```tsx
109import { cn } from "@/lib/utils"
110
111interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
112 variant?: "default" | "destructive" | "outline" | "ghost"
113 size?: "sm" | "md" | "lg"
114}
115
116export function Button({
117 className,
118 variant = "default",
119 size = "md",
120 ...props
121}: ButtonProps) {
122 return (
123 <button
124 className={cn(
125 "inline-flex items-center justify-center rounded-md font-medium transition-colors",
126 "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
127 "disabled:pointer-events-none disabled:opacity-50",
128 {
129 "bg-zinc-900 text-white hover:bg-zinc-800 dark:bg-zinc-50 dark:text-zinc-900":
130 variant === "default",
131 "bg-red-600 text-white hover:bg-red-700": variant === "destructive",
132 "border border-zinc-300 bg-transparent hover:bg-zinc-100":
133 variant === "outline",
134 "hover:bg-zinc-100 dark:hover:bg-zinc-800": variant === "ghost",
135 },
136 {
137 "h-8 px-3 text-sm": size === "sm",
138 "h-10 px-4 text-sm": size === "md",
139 "h-12 px-6 text-base": size === "lg",
140 },
141 className
142 )}
143 {...props}
144 />
145 )
146}
147```
148
149### TypeScript
150
151- Prefer `interface` for object shapes that may be extended; `type` for unions, intersections, and computed types.
152- Export types from `types/` or colocate them with the module that owns them.
153- Never use `as` to silence the compiler. Fix the type or use a type guard.
154- Use `satisfies` when you want type checking without widening.
155- Function return types: let them be inferred for simple functions; annotate explicitly for exported functions and anything non-trivial.
156
157### Tailwind CSS
158
159- **No magic numbers.** Use Tailwind's spacing/sizing scale. If you need a custom value, extend the theme in `tailwind.config.ts`, don't use arbitrary values like `w-[347px]`.
160- **Responsive:** mobile-first. Use `sm:`, `md:`, `lg:` breakpoints intentionally.
161- **Dark mode:** always provide dark variants for colors and borders. Use `dark:` prefix.
162- **Avoid `@apply`** unless you're styling prose/markdown content or third-party elements you can't add classes to.
163- **Component variants:** use `cn()` with conditional objects (see Button example), not multiple ternary strings.
164
165### Data Fetching & Caching
166
167- Fetch data in Server Components. Pass results down as props.
168- Use `fetch()` with Next.js caching options or `unstable_cache` for database queries.
169- Use `revalidatePath` / `revalidateTag` in Server Actions after mutations.
170- Route Handlers (`app/api/`) are for webhooks, third-party callbacks, and external API consumption. Don't use them for internal data fetching — use Server Components or Server Actions.
171
172### Server Actions
173
174- Define with `"use server"` at the top of the function or file.
175- Validate ALL input with Zod before doing anything.
176- Return typed result objects, not thrown errors:
177
178```ts
179"use server"
180
181import { z } from "zod"
182import { revalidatePath } from "next/cache"
183import { db } from "@/lib/db"
184
185const schema = z.object({
186 title: z.string().min(1).max(200),
187 content: z.string().min(1),
188})
189
190type ActionResult =
191 | { success: true; id: string }
192 | { success: false; error: string }
193
194export async function createPost(formData: FormData): Promise<ActionResult> {
195 const parsed = schema.safeParse({
196 title: formData.get("title"),
197 content: formData.get("content"),
198 })
199
200 if (!parsed.success) {
201 return { success: false, error: parsed.error.issues[0].message }
202 }
203
204 const post = await db.post.create({ data: parsed.data })
205 revalidatePath("/posts")
206 return { success: true, id: post.id }
207}
208```
209
210### File & Naming Conventions
211
212- **Files:** kebab-case for everything (`user-profile.tsx`, `auth-utils.ts`).
213- **Components:** PascalCase exports (`export function UserProfile()`).
214- **One component per file** for anything non-trivial. Small helpers can be colocated.
215- **Barrel exports:** avoid `index.ts` re-export files. Import directly from the source module.
216- **Route files:** follow Next.js conventions exactly (`page.tsx`, `layout.tsx`, `loading.tsx`, `error.tsx`, `not-found.tsx`).
217
218### Error Handling
219
220- Use `error.tsx` boundaries at the route segment level.
221- Use `not-found.tsx` + `notFound()` from `next/navigation`.
222- Never silently swallow errors. Log them, then show a user-friendly message.
223- In Server Actions, return structured errors (see above). Don't throw.
224
225### Performance
226
227- Use `loading.tsx` and `<Suspense>` for streaming.
228- Use `next/image` for all images. Specify `width`/`height` or use `fill` with a sized container.
229- Use `next/font` for fonts. No external font CDN links.
230- Dynamic imports (`next/dynamic`) for heavy client components not needed on initial render.
231- Keep `"use client"` boundaries as low in the tree as possible.
232
233## Testing
234
235- **Unit tests:** Vitest for utilities and Zod schemas.
236- **Component tests:** React Testing Library. Test behavior, not implementation.
237- **Don't mock** what you don't own unless you have to. Prefer testing against real behavior.
238- **File naming:** `*.test.ts` / `*.test.tsx` colocated with the source file.
239- **Run tests:** `pnpm test` / `pnpm test:watch`
240
241## Commands
242
243```bash
244pnpm dev # Start dev server
245pnpm build # Production build (catches type errors)
246pnpm lint # ESLint
247pnpm test # Run tests
248pnpm test:watch # Watch mode
249pnpm dlx @next/bundle-analyzer # Analyze bundle (if configured)
250```
251
252## When Generating Code
253
2541. **Search the codebase first.** Before creating a new component or utility, check if one already exists. Use Cursor's codebase search.
2552. **Follow existing patterns.** Match the style, structure, and conventions already in the repo. Consistency beats personal preference.
2563. **Don't add libraries** without asking. If a dependency would help, suggest it with rationale — don't just install it.
2574. **Keep changes minimal.** When editing existing files, change only what's needed. Don't refactor unrelated code in the same edit.
2585. **Explain trade-offs** when there's a meaningful choice (RSC vs client component, cache strategy, etc.).
2596. **Prefer composition over abstraction.** A few explicit lines beat a clever wrapper nobody will understand next month.
260