// 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--path=
.cursor/rules/
// File Content
.cursor/rules (MDC)
1---2description: Next.js + TypeScript + Tailwind conventions for App Router and server-action-first workflows.3globs:4alwaysApply: true5---67# .cursorrules — Next.js + TypeScript + Tailwind CSS89You 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.1011## Quick Reference1213| 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/`) |2728## Project Structure2930```31├── app/32│ ├── layout.tsx # Root layout (Server Component)33│ ├── page.tsx # Home route34│ ├── globals.css # Tailwind directives + custom CSS35│ ├── (marketing)/ # Route group — public pages36│ ├── (app)/ # Route group — authenticated pages37│ │ ├── dashboard/38│ │ │ ├── page.tsx39│ │ │ └── loading.tsx40│ │ └── layout.tsx41│ └── api/ # Route handlers (use sparingly)42├── components/43│ ├── ui/ # Primitive/reusable UI (buttons, inputs, cards)44│ └── [feature]/ # Feature-scoped components45├── lib/46│ ├── utils.ts # cn() helper and shared utilities47│ ├── validations/ # Zod schemas48│ └── [service].ts # Service-layer modules (db, auth, email)49├── hooks/ # Custom React hooks (client-only)50├── types/ # Shared TypeScript types/interfaces51├── public/ # Static assets52├── tailwind.config.ts53├── next.config.ts54└── tsconfig.json55```5657## Tech Stack Assumptions5859- **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.6566## Core Conventions6768### Server Components First6970Every 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?7172```tsx73// ✅ Server Component — default, no directive needed74import { db } from "@/lib/db"75import { LikeButton } from "./like-button" // client island7677export async function PostCard({ id }: { id: string }) {78 const post = await db.post.findUnique({ where: { id } })79 if (!post) return null8081 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```9293### The `cn()` Helper9495Always use `cn()` for conditional/merged class names. It lives in `lib/utils.ts`:9697```ts98import { type ClassValue, clsx } from "clsx"99import { twMerge } from "tailwind-merge"100101export function cn(...inputs: ClassValue[]) {102 return twMerge(clsx(inputs))103}104```105106Use it any time you accept `className` as a prop or conditionally apply classes:107108```tsx109import { cn } from "@/lib/utils"110111interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {112 variant?: "default" | "destructive" | "outline" | "ghost"113 size?: "sm" | "md" | "lg"114}115116export function Button({117 className,118 variant = "default",119 size = "md",120 ...props121}: ButtonProps) {122 return (123 <button124 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 className142 )}143 {...props}144 />145 )146}147```148149### TypeScript150151- 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.156157### Tailwind CSS158159- **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.164165### Data Fetching & Caching166167- 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.171172### Server Actions173174- 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:177178```ts179"use server"180181import { z } from "zod"182import { revalidatePath } from "next/cache"183import { db } from "@/lib/db"184185const schema = z.object({186 title: z.string().min(1).max(200),187 content: z.string().min(1),188})189190type ActionResult =191 | { success: true; id: string }192 | { success: false; error: string }193194export async function createPost(formData: FormData): Promise<ActionResult> {195 const parsed = schema.safeParse({196 title: formData.get("title"),197 content: formData.get("content"),198 })199200 if (!parsed.success) {201 return { success: false, error: parsed.error.issues[0].message }202 }203204 const post = await db.post.create({ data: parsed.data })205 revalidatePath("/posts")206 return { success: true, id: post.id }207}208```209210### File & Naming Conventions211212- **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`).217218### Error Handling219220- 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.224225### Performance226227- 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.232233## Testing234235- **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`240241## Commands242243```bash244pnpm dev # Start dev server245pnpm build # Production build (catches type errors)246pnpm lint # ESLint247pnpm test # Run tests248pnpm test:watch # Watch mode249pnpm dlx @next/bundle-analyzer # Analyze bundle (if configured)250```251252## When Generating Code2532541. **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