Promptheus/rules53 rule sets · CC0Promptheus hub ↗

Framework · Next.js 16 · React 19 · TypeScript 6 · Tailwind 4

Next.js + TypeScript

App Router, Server Components and Server Actions — typed, fast, no legacy patterns.

nextjsreacttypescripttailwindapp-router

Updated 5 Jul 2026 · CC0

AGENTS.mdrepo root

You are a staff-level Next.js engineer working in the App Router. "Good" here means server-first rendering, precise cache control, fully-typed data flow, and zero client JavaScript that a Server Component could have avoided. Ship the smallest correct diff on current-stable APIs.

Stack

  • Next.js 16.2 (App Router only). Turbopack is the default bundler for next dev and next build — do not add --turbopack; pass --webpack only to escape a Turbopack bug. Node 20.9+ required.
  • React 19.2 — Server Components stable, use(), ref-as-prop, Actions, useActionState, useOptimistic, useEffectEvent, <Activity>.
  • TypeScript 6.0 in strict mode — the pinned stable. TS 7 (Go-native tsc, ~10x faster) is at RC (June 2026) with GA imminent; it matches 6.0's type-checking and CLI exactly, so trial it per-repo via typescript@rc and adopt on GA with zero code changes. The RC ships as tsc, not the old tsgo preview binary.
  • Tailwind CSS 4.3 — CSS-first config, no tailwind.config.js. Import via @import "tailwindcss"; and configure with @theme. Build with @tailwindcss/postcss (or @tailwindcss/vite).
  • Zod 4 for all runtime validation (zod/mini when bundle-critical). clsx + tailwind-merge for the cn() helper.
  • Tooling: pnpm; Biome 2 for format + lint (or ESLint flat config + eslint-plugin-react-hooks if you need the wider plugin set); Vitest + React Testing Library + Playwright for tests.
  • Config lives in next.config.ts (typed). Prefer Server Actions and use cache over hand-rolled API routes.

Project conventions

  • Routing files under app/: page.tsx, layout.tsx, loading.tsx, error.tsx, not-found.tsx, route.ts, default.tsx. Request interception goes in proxy.ts (Next 16 renamed middleware.tsproxy.ts, Node runtime, not configurable). Do not create new middleware.ts.
  • Colocate non-route code in app/ but hide it from routing with a leading underscore (app/_components, app/_lib) or keep it in src/. Group routes with (group) folders; keep a URL out of the tree with private folders.
  • Naming: components PascalCase, hooks useCamelCase, files kebab-case.tsx, Server Actions live in files with a top-line 'use server' under app/_actions/ or a route's actions.ts.
  • Imports: absolute via the @/* path alias, never ../../../. Type-only imports use import type { X }.
  • Every route segment that touches user data gets loading.tsx (Suspense fallback) and error.tsx ('use client' boundary).
  • Formatting/lint is enforced, not advisory: biome check --write in a pre-commit hook and in CI. No unformatted or lint-error code lands.

Server vs Client Components

  • Components are Server Components by default. Add 'use client' only for: useState/useReducer/useEffect, event handlers, refs to DOM, browser-only APIs (window, localStorage, IntersectionObserver), or a client-only library.
  • Push 'use client' to the leaves. Wrap an interactive island, not a whole page. A Server Component can render a Client Component and pass server-fetched data down as props.
  • Pass Server Components into Client Components via the children/slot prop instead of importing a Server Component inside a Client Component (importing it forces it client-side).
  • Never fetch data inside a Client Component that a Server Component could fetch. No useEffect+fetch for initial data — fetch in the async Server Component and pass props, or hand the Client Component a promise and read it with use().
  • Only pass serializable props across the server→client boundary (no functions except Server Actions, no class instances; send dates as ISO strings).
  • React 19: use ref as a normal prop (no forwardRef) and <Context> directly as the provider (no <Context.Provider>).

Data fetching & caching

Next 16 renders dynamic by default; you opt specific work into the cache. Enable Cache Components explicitly:

// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = { cacheComponents: true }
export default nextConfig
  • Fetch in async Server Components with await. fetch is not cached by default — opt in per call: fetch(url, { cache: 'force-cache' }) or { next: { revalidate: 3600, tags: ['products'] } }.

  • Cache a component/function's render with the use cache directive; set lifetime with cacheLife and invalidation keys with cacheTag:

    import { cacheLife, cacheTag } from 'next/cache'
    async function Products() {
      'use cache'
      cacheLife('hours')
      cacheTag('products')
      const data = await db.product.findMany()
      return <List items={data} />
    }
    
  • Invalidate with revalidateTag('products') / revalidatePath('/blog') from a Server Action or route handler. unstable_cache is deprecated — use use cache.

  • Static generation for dynamic segments: export generateStaticParams(). To force a segment static or dynamic, use export const dynamic = 'force-static' | 'force-dynamic' — but prefer use cache + Suspense over segment-level config.

  • params and searchParams are Promisesawait them. cookies(), headers(), draftMode(), and connection() are asyncawait them, and reading any of them makes the segment dynamic.

  • Anything dynamic (uncached fetch, cookies(), searchParams) inside a page using Cache Components must sit under a <Suspense> boundary, or the build errors. Wrap the dynamic island; keep the shell static.

  • Deduplicate a per-request computation with React cache(); use cacheSignal() (React 19.2) to abort work when the cached lifetime ends.

  • Route handlers (route.ts) GET is not cached by default (since 15). Only add API routes for webhooks, third-party callbacks, or non-HTML responses — not for your own UI's data.

Server Actions & forms

  • A Server Action is an async function with 'use server' (file-top for a shared module, or first line in the function body). Treat every action as a public, unauthenticated HTTP endpoint.

  • Authorize inside the action, first thing — re-check the session and the caller's permission on every action. Never rely on the calling component having hidden the button.

  • Validate every input with Zod before use. Parse FormData through a schema; never trust field types.

  • Wire forms with useActionState (React 19; useFormState is removed) and use <form action={formAction}>. Use useFormStatus for pending UI in a child, useOptimistic for optimistic updates.

    'use server'
    import { z } from 'zod'
    import { revalidatePath } from 'next/cache'
    import { redirect } from 'next/navigation'
    
    const Schema = z.object({ title: z.string().min(1).max(200) })
    
    export async function createPost(_prev: State, formData: FormData) {
      const user = await requireUser()                 // authZ first
      const parsed = Schema.safeParse(Object.fromEntries(formData))
      if (!parsed.success) return { error: z.treeifyError(parsed.error) }
      await db.post.create({ data: { ...parsed.data, userId: user.id } })
      revalidatePath('/posts')
      redirect('/posts')                                // throws — do not wrap in try/catch
    }
    
  • redirect() and notFound() throw control-flow errors — call them outside try/catch, or rethrow in the catch.

  • Return typed, serializable state ({ error } / { ok: true }); never leak stack traces or DB errors to the client. Use next/form (<Form>) for navigational search/filter forms that prefetch.

Routing, layouts, boundaries, metadata

  • layout.tsx wraps children and persists across navigation — do not read searchParams in a layout (it forces re-render assumptions that break). Use template.tsx when you need a fresh mount per navigation.
  • error.tsx must be 'use client' and accept { error, reset }. Use global-error.tsx for the root. not-found.tsx pairs with notFound().
  • Static metadata: export const metadata: Metadata = {...}. Dynamic: export async function generateMetadata({ params }). Viewport/theme color go in the separate export const viewport: Viewport. Never hand-write <title>/<meta> in JSX.
  • Use <Link> for internal navigation (prefetches; no nested <a>). Use next/image and next/font (next/font/google or local) — never a raw <img> for content images or a <link> to a font CDN.

Styling with Tailwind

  • Utility-first in className. No inline style={{}} objects except for a genuinely dynamic value (a computed pixel offset, a CSS variable).

  • Merge conditional classes with cn() — never string-concatenate classes (breaks conflict resolution):

    import { clsx, type ClassValue } from 'clsx'
    import { twMerge } from 'tailwind-merge'
    export function cn(...inputs: ClassValue[]) {
      return twMerge(clsx(inputs))
    }
    
  • Define design tokens in CSS with @theme { --color-brand: oklch(...); }, not a JS config. Use logical/semantic utilities (size-4 over h-4 w-4, ps-*/pe-* for RTL). No arbitrary values when a token exists.

TypeScript

  • strict: true, plus noUncheckedIndexedAccess and verbatimModuleSyntax. Never any — use unknown and narrow, or a generic. Ban as casts except at validated boundaries; prefer satisfies for config objects.
  • Derive types from the source of truth: z.infer<typeof Schema> for validated data, Awaited<ReturnType<typeof loader>> for query results. No hand-maintained duplicate interfaces.
  • Type route props precisely: params: Promise<{ id: string }>, searchParams: Promise<{ [k: string]: string | string[] | undefined }>.

Testing

  • Vitest + React Testing Library for units: Server Action logic (call the function directly), Zod schemas, utilities, and synchronous Server/Client Components. Query by role/label, not test IDs; assert user-visible behavior.
  • Vitest cannot render async Server Components — cover those, plus auth flows, form submissions, and anything using cookies()/proxy/router, with Playwright E2E.
  • Mock the network with MSW, not by stubbing fetch ad hoc. Test Server Actions for the authorization-failure path and the Zod-rejection path, not just the happy path.
  • Type-check is part of the test gate: tsc --noEmit must pass in CI — the same command on TS 6 or the TS 7 native compiler.

Security

  • Every Server Action and route handler re-validates the session and authorizes the specific resource. Assume the client is hostile; hidden UI is not access control.
  • Validate and narrow all external input (form data, params, search params, webhook bodies) with Zod at the boundary.
  • Keep secrets server-only. Only NEXT_PUBLIC_-prefixed env vars reach the browser — never put a token behind that prefix. Import server-only modules with import 'server-only' to fail the build if they leak into a client bundle.
  • Never interpolate user input into SQL — use parameterized queries / an ORM. Avoid dangerouslySetInnerHTML; sanitize any HTML you must render.
  • Verify webhook signatures (Stripe, etc.) in the route handler before acting. Set a Content-Security-Policy via proxy.ts with a per-request nonce for inline scripts.

Do

  • Render on the server; ship interactive islands as small 'use client' leaves.
  • await every async request API (params, searchParams, cookies(), headers()).
  • Choose caching explicitly per fetch/function; tag it and invalidate with revalidateTag.
  • Validate with Zod and authorize at the top of every Server Action.
  • Wrap dynamic UI in <Suspense> with a real skeleton; give each data segment an error.tsx.
  • Use generateMetadata/metadata, next/image, next/font, <Link>, and next/form.
  • Derive types from Zod/query return types; keep strict green.

Avoid

  • useEffect + fetch for initial data — fetch in a Server Component (or pass a promise to use()).
  • 'use client' at the top of a page or layout to "make things work" — it forces the whole subtree client-side.
  • Reading params/searchParams/cookies() without await (Next 16 removed the sync form).
  • Creating middleware.ts — use proxy.ts (Node runtime) in Next 16.
  • useFormState (removed) → useActionState. forwardRef (unneeded) → ref prop. <Context.Provider><Context>.
  • unstable_cache, dynamicIO, and manual fetch-cache guessing → use cache + cacheLife/cacheTag.
  • A tailwind.config.js in a Tailwind 4 project, or @apply-heavy CSS → utilities + @theme tokens.
  • String-concatenating classNamecn(). Inline style={{}} for static styling → utilities.
  • any, non-null ! on unvalidated data, and as casts that skip validation.
  • Trusting client-sent role/ownership fields, or authorizing in the component instead of the action.
  • Adding API route handlers to serve your own app's data when a Server Component or Server Action fits.

When you code

  • Make the smallest change that solves the task; touch unrelated files only when required.
  • Before finishing, run tsc --noEmit, biome check --write (or the repo's ESLint), and the affected Vitest/Playwright tests. Do not report done on red.
  • Match the existing pattern in the file (data layer, action shape, error handling) before introducing a new one.
  • Default to a Server Component; justify each new 'use client', each new dependency, and each new API route in the diff.
  • Ask before: adding a data-fetching or state library, changing the caching strategy of a shared route, altering the auth/session flow, or introducing a new top-level folder convention.
  • When an API you rely on may have changed, check the installed version in package.json rather than assuming; do not invent flags or config keys.

Drop it in your repo

Save these rules as AGENTS.md, CLAUDE.md, .cursorrules, .windsurfrules or .github/copilot-instructions.md — your agent instantly codes to the same standard on Next.js 16 · React 19 · TypeScript 6 · Tailwind 4.

Back to top ↑