---
name: "typescript-nextjs-rules"
description: "Context rules for AI agents working in TypeScript/Next.js App Router projects. Covers strict typing, server vs client components, Tailwind, Zod, and file conventions."
metadata:
  version: "1.0.0"
context-only: true
---

# TypeScript Next.js Rules

> **Context skill:** Install this into your agent to enforce consistent, production-quality patterns in TypeScript Next.js App Router projects. Works with Claude Code, Cursor, Windsurf, and any agent that reads context files.

---

## Stack Assumptions

- **Next.js 14+** with App Router (not Pages Router)
- **TypeScript** with strict mode enabled (`"strict": true` in tsconfig)
- **Tailwind CSS** for styling
- **Zod** for runtime validation
- **Supabase or Prisma** for database (adapt as needed)

---

## File & Directory Conventions

```
app/
  (group)/          ← Route groups (no URL segment)
    layout.tsx      ← Shared layout
    page.tsx        ← Route page (server component by default)
    loading.tsx     ← Suspense fallback
    error.tsx       ← Error boundary ('use client' required)
  api/
    route.ts        ← API routes (GET, POST, etc. as named exports)
components/
  ui/               ← Primitive UI: Button, Card, Badge, etc.
  feature/          ← Feature-specific composed components
lib/
  utils/            ← Pure utility functions (no React)
  types/            ← TypeScript type definitions
  actions/          ← Server Actions
hooks/              ← Custom React hooks (client-side)
```

---

## Server vs Client Components

**Default to Server Components.** Only add `'use client'` when you need:
- `useState` or `useReducer`
- `useEffect` or lifecycle hooks
- Browser APIs (`window`, `localStorage`, etc.)
- Event handlers that run in the browser
- Third-party client-only libraries

**Never** put `'use client'` at the top of a layout file.

---

## TypeScript Rules

```typescript
// ✅ Always type function return values explicitly
async function getUser(id: string): Promise<User | null> { ... }

// ✅ Use `type` for unions/intersections, `interface` for object shapes
type Status = 'pending' | 'active' | 'archived'
interface UserProfile { id: string; email: string; }

// ✅ Never use `any` — use `unknown` and narrow
function parse(input: unknown): string {
  if (typeof input !== 'string') throw new Error('Expected string')
  return input
}

// ❌ Never
const data: any = response.json()

// ✅ Type API responses with Zod
const UserSchema = z.object({ id: z.string().uuid(), email: z.string().email() })
type User = z.infer<typeof UserSchema>
```

---

## API Routes

```typescript
// app/api/users/route.ts
import { NextResponse } from 'next/server'
import { z } from 'zod'

const CreateUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1).max(100),
})

export async function POST(request: Request) {
  const body = await request.json()
  const result = CreateUserSchema.safeParse(body)
  if (!result.success) {
    return NextResponse.json({ error: result.error.flatten() }, { status: 422 })
  }
  // ... proceed with result.data (fully typed)
}
```

---

## Data Fetching

```typescript
// ✅ Server Component data fetching — no useEffect
export default async function UsersPage() {
  const users = await getUsers() // Direct async call in component
  return <UserList users={users} />
}

// ✅ Parallel fetching with Promise.all
const [user, posts] = await Promise.all([getUser(id), getPosts(id)])

// ✅ Use `cache` for request deduplication
import { cache } from 'react'
const getUser = cache(async (id: string) => { ... })
```

---

## Error Handling

```typescript
// ✅ Never throw in Server Components — use error.tsx
// app/(main)/error.tsx
'use client'
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
  return <div><h2>Something went wrong</h2><button onClick={reset}>Try again</button></div>
}

// ✅ API routes always return typed error shapes
return NextResponse.json({ error: 'Not found', code: 'USER_NOT_FOUND' }, { status: 404 })
```

---

## Tailwind CSS

- Use Tailwind utility classes directly — no custom CSS unless absolutely necessary
- Use `cn()` helper (from `clsx` + `tailwind-merge`) to conditionally join classes
- Dark mode via `dark:` variants (not separate stylesheets)
- Never use inline `style={{}}` for values that Tailwind covers
- Use `@apply` sparingly — only in component-level `.module.css` files if needed

---

## Do Not

- Do not use `pages/` directory — App Router only
- Do not import server-only code into client components (will cause runtime errors)
- Do not use `getServerSideProps` or `getStaticProps` — use async Server Components
- Do not `console.log` in production code — use a proper logger
- Do not hardcode environment variables — use `process.env.VAR` with validation on startup
- Do not use default exports for utility functions — named exports only

## Playground

<!DOCTYPE html><html><head><meta charset='utf-8'><style>*{box-sizing:border-box;margin:0;padding:0}body{background:#0d1117;font-family:monospace;font-size:11px;height:100vh;display:flex;flex-direction:column;overflow:hidden;padding:12px}.title{color:#58a6ff;font-size:13px;font-weight:bold;margin-bottom:6px}.subtitle{color:#8b949e;font-size:10px;margin-bottom:10px}.rule{display:flex;align-items:flex-start;gap:8px;margin-bottom:7px;line-height:1.5}.dot{color:#3fb950;font-size:14px;line-height:1.3;flex-shrink:0}.text{color:#e6edf3}.hl{color:#3fb950}.section{color:#e3b341;font-size:10px;text-transform:uppercase;letter-spacing:.08em;margin:10px 0 5px}</style></head><body><div class='title'>TypeScript · Next.js Rules</div><div class='subtitle'>Active in every session — enforced automatically</div><div class='section'>Types</div><div class='rule'><span class='dot'>●</span><div class='text'>No <span class='hl'>any</span> — use <span class='hl'>unknown</span> with narrowing or explicit generics</div></div><div class='rule'><span class='dot'>●</span><div class='text'>Prefer <span class='hl'>interface</span> for object shapes, <span class='hl'>type</span> for unions/aliases</div></div><div class='rule'><span class='dot'>●</span><div class='text'>All <span class='hl'>async</span> functions return <span class='hl'>Promise&lt;T&gt;</span> — never implicit any</div></div><div class='section'>Next.js</div><div class='rule'><span class='dot'>●</span><div class='text'>Server Components by default — add <span class='hl'>'use client'</span> only when needed</div></div><div class='rule'><span class='dot'>●</span><div class='text'>Data fetching in Server Components via <span class='hl'>fetch()</span> with cache options</div></div><div class='rule'><span class='dot'>●</span><div class='text'>API routes: validate input with <span class='hl'>zod</span> before touching the DB</div></div><div class='section'>Style</div><div class='rule'><span class='dot'>●</span><div class='text'>Tailwind only — no inline styles, no CSS modules</div></div><div class='rule'><span class='dot'>●</span><div class='text'>Imports: external → internal → relative (enforced by ESLint)</div></div></body></html>