Next.js + TypeScript
Project context
This is a Next.js 15 App Router project written in TypeScript with strict mode. It's a production web application — favor stability and explicitness over cleverness. The codebase is shared across multiple developers and AI tools; consistency matters more than personal preference.
Stack
- Runtime: Node.js 20+ (LTS)
- Framework: Next.js 15 (App Router only — never Pages Router)
- UI: React 19 (with Server Components by default)
- Language: TypeScript 5+ in strict mode (
"strict": true,"noUncheckedIndexedAccess": true) - Styling: Tailwind CSS 4 with semantic CSS custom properties
- Package manager: pnpm (do not use npm or yarn)
- Linting: ESLint with
eslint-config-next - Formatting: Prettier (run automatically via the editor / pre-commit hook)
- Testing: Vitest for unit tests, Playwright for e2e
Folder structure
src/
app/ — App Router routes (pages, layouts, loading, error, route handlers)
components/ — Reusable React components, organized by feature subdir
lib/ — Utilities and integrations, organized by feature subdir
data/ — Static data and config files (TypeScript const arrays preferred)
content/ — Static markdown / MDX content
hooks/ — Reusable client-side hooks (only when shared across components)
styles/ — Global styles, theme tokens (most styling lives in Tailwind classes)
public/ — Static assets served at the root
tests/ — Test utilities and Playwright tests (unit tests live next to source)
Routes live under src/app/. Components specific to one feature live under src/components/<feature>/. Cross-cutting components live at the top of src/components/.
Code style
- Functional components only — no class components
- Named exports only — no default exports except where Next.js requires them (
page.tsx,layout.tsx,error.tsx,loading.tsx,not-found.tsx,opengraph-image.tsx,route.ts) - File names:
kebab-casefor utilities,PascalCase.tsxfor components - Imports: absolute paths via
@/alias, never relative paths that climb (../../../) - Group imports: external packages, then
@/...internal, then relative - Prefer
constoverlet; nevervar - Prefer arrow functions for callbacks; named
functiondeclarations for top-level helpers - Type everything. Use
unknownoverany. If you reach forany, find another way
Patterns to follow
Server Components by default. Add "use client" only when the component needs state, effects, browser APIs, or event handlers. Most data fetching belongs in server components.
Async server components for data fetching.
export default async function Page() {
const data = await fetchSomething()
return <ClientUI initialData={data} />
}
Server Actions for mutations. Prefer Server Actions over route handlers for form submissions and mutations. Place them in a co-located actions.ts file with 'use server' at the top. Validate inputs with Zod at the action boundary.
Route Handlers for public APIs. Use route handlers (route.ts) only for public APIs (webhooks, RSS feeds, well-known endpoints). For internal mutations, use Server Actions.
Tailwind for styling. No CSS modules, no styled-components, no inline style props except for dynamic values that can't be expressed as classes. Use semantic color tokens (bg-surface, text-on-surface-variant) — never raw hex.
Composition over abstraction. Duplicate twice before extracting a component or hook. Premature abstraction is harder to undo than duplication.
Loading and error states are required. Every async route gets a loading.tsx. Every page gets an error.tsx boundary at its segment level.
Patterns to avoid
- Pages Router patterns. No
getServerSideProps,getStaticProps,getInitialProps. They don't exist in App Router. useEffectfor data fetching. Fetch in server components or use a Server Action.useEffect(() => fetch())in client components leaks waterfalls and breaks SSR.any. Useunknownand narrow.- Default exports. They make refactoring and grep harder.
- Client components that don't need to be. Mark
"use client"at the smallest leaf, not the layout root. - Inline server code in client components.
import "server-only"at the top of any module that must never ship to the browser. router.pushfor full navigations. Use<Link>for navigation;router.pushis for programmatic navigation (after a form action, etc.).
Testing
- Unit tests live alongside source:
foo.ts→foo.test.ts - Use Vitest with
@testing-library/reactfor component tests - Mock at the boundary — don't mock internal modules; mock
fetch,fs, etc. at the system edge - Playwright tests live in
tests/e2e/and exercise critical user flows only - Run
pnpm testbefore considering a task complete
Tooling
pnpm dev— Next.js dev server with Turbopackpnpm build— production buildpnpm lint— ESLintpnpm test— Vitestpnpm e2e— Playwrightpnpm typecheck—tsc --noEmit
Run lint, typecheck, and test before considering any task done.
AI behavioral rules
- Ask before installing dependencies. Surface the exact package and reason; let the human approve.
- Run the linter and tests after editing. Don't claim a task is complete if either is failing.
- Never modify migrations after they're committed. Generate a new migration instead.
- Don't refactor unrelated code in the same change. One concern per change.
- No comments unless they explain the why. Code names should describe what; only add comments for non-obvious motivation, hidden constraints, or workarounds.
- No
console.login committed code. Remove debug logs before considering a task done. - Prefer editing existing files over creating new ones. Only create files when the new concern doesn't fit anywhere existing.
- When in doubt, stop and ask. Better to ask one clarifying question than to ship a wrong implementation that needs a follow-up.