TypeScript
TypeScript rules and best practices for Claude Code.
43.7k
- TypeScript
- Next.js
- React
- Node.js
- Zod
- Vite
- Tailwind
20 TEMPLATES
Next.js App Router + TypeScript Rules
# CLAUDE.md — Next.js App Router + TypeScript ## Routing & rendering - App Router only. Do not create files under `pages/`. - Server Components by default. Add `'use client'` only when you need state, refs, browser APIs, or event handlers. - Co-locate route segments under `app/`. Route handlers go in `route.ts`, pages in `page.tsx`, layouts in `layout.tsx`. - Use `loading.tsx`, `error.tsx`, and `not-found.tsx` for built-in boundaries — don't roll your own when the file convention exists. - Default to streaming with `<Suspense>` instead of blocking the entire route on slow data. ## Data fetching - Fetch in Server Components with `async/await`. Pass data down as props; don't refetch the same data on the client. - Use Server Actions for mutations. Return typed results — never throw raw errors back to the client. - Cache deliberately: choose between `force-cache`, `no-store`, or `revalidate: N` per `fetch` call. Don't leave it implicit. - Tag cache reads (`next: { tags: ['user-list'] }`) and invalidate them with `revalidateTag` after mutations. ## TypeScript - `strict: true` in `tsconfig.json`. No `any`, no `as` casts unless they're narrowing a `unknown`. - Type the `params` and `searchParams` of every page and layout — these are `Promise`s in current Next. - Validate every untrusted input (form data, search params, route handler bodies) with Zod before using it. ## Styling & UI - Tailwind for layout and spacing. No CSS-in-JS for static styles. - Keep one global stylesheet (`app/globals.css`) — no per-component CSS files. - Images: always use `next/image` with explicit width/height or `fill` + a sized parent. ## SEO - Export `metadata` (or `generateMetadata`) from every page. Set `title`, `description`, and `alternates.canonical`. - Generate sitemaps in `app/sitemap.ts` from your data sources, not by hand. - Use `app/[route]/opengraph-image.tsx` to ship per-page OG images, not a single shared one. ## Don't - Don't use `getServerSideProps`, `getStaticProps`, or `getInitialProps` — they don't exist in App Router. - Don't import server-only modules (`fs`, `db client`, etc.) into client components. - Don't disable TypeScript or ESLint errors to ship. - Don't use `useEffect` to fetch data you could fetch on the server.React + TypeScript + Vite SPA Rules
# CLAUDE.md — React + TypeScript + Vite ## Project structure - `src/components/` — reusable components, server-agnostic - `src/features/<feature>/` — feature-scoped components, hooks, types, services - `src/lib/` — pure utilities, no React imports - `src/hooks/` — shared custom hooks (one hook per file) - `src/app/` — top-level routing and providers - Co-locate tests next to the file (`Component.test.tsx`). ## TypeScript - `strict: true` plus `noUncheckedIndexedAccess` and `exactOptionalPropertyTypes`. - No `any`. Use `unknown` and narrow. - Component props are an `interface`, not a `type` alias, when other components might extend them. - Children: `children: React.ReactNode` for free-form, `children: React.ReactElement` for a single element. ## Components - Function components only. No class components. - Default to named exports. One component per file. The file name matches the component. - Keep components under ~150 lines. Split by responsibility, not by size alone. - Server-state, async work, and side effects belong in hooks — never inline `useEffect` for fetching. ## Hooks - Custom hook names start with `use`. They return either an object (multiple values) or a single value — never both. - Always specify the dep array. ESLint `react-hooks/exhaustive-deps` is `error`, not `warn`. - `useEffect` is a last resort. Prefer derived state, `useMemo`, or moving logic to event handlers. ## State - Local state with `useState`/`useReducer`. - Cross-component state with React Context only when prop-drilling exceeds ~3 levels. - Server state with TanStack Query (don't roll your own cache). - Global UI state (modals, toasts) with Zustand or context — keep stores small and feature-local. ## Suspense & errors - Wrap async boundaries in `<Suspense>` with a real fallback (skeletons match the layout). - Wrap routes in `<ErrorBoundary>`. Each boundary owns a recovery action ("Retry", "Go home"). ## Vite - Pin Vite and `@vitejs/plugin-react` versions. Don't auto-update on every PR. - Use `import.meta.env` (not `process.env`). Prefix public env vars with `VITE_`. - Add `vitest` for tests. Reuse Vite's config — no separate Jest setup. ## Don't - Don't put fetches in `useEffect`. Use TanStack Query or move to a Suspense data layer. - Don't pass JSX as a prop just to defer rendering. Use `<Suspense>`. - Don't disable lint rules to silence "missing dep" warnings — fix the underlying bug. - Don't reach for `useCallback` / `useMemo` until profiling proves they help.TypeScript Strict Rules
# CLAUDE.md — TypeScript Strict Rules ## tsconfig - `strict: true` is the floor. Add the rest of the strictness flags: ```json { "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true, "noImplicitOverride": true, "noFallthroughCasesInSwitch": true, "useUnknownInCatchVariables": true, "verbatimModuleSyntax": true } } ``` - ESLint `@typescript-eslint/no-explicit-any` is `error`, not `warn`. - `noEmitOnError: true` so a compile error fails the build. ## Anti-`any` - `any` is banned. Use `unknown` and narrow. - Stuck on a third-party type? Write a `.d.ts` declaration that's tighter than the upstream. - `// @ts-expect-error` over `// @ts-ignore` — it errors when the issue is fixed, telling you to remove it. ## Type narrowing - Use type predicates (`x is Foo`) for custom narrows. They're functions returning a typed boolean. - Discriminated unions with a literal `kind` / `type` tag for state machines and result types: ```ts type Result<T> = { ok: true; value: T } | { ok: false; error: string } ``` - `switch` on the discriminator with `default: assertNever(x)` for exhaustiveness. ## `as` casts - `as` is a sharp tool. Allow only: - Narrowing `unknown` after a runtime check - `const` assertions: `as const` - Casting an empty array to a specific tuple type - Forbidden: `as Foo`, `as unknown as Bar`. If you need it, you're missing a runtime check or a type predicate. ## Branded types - Use branded types for IDs and other primitives that look the same but aren't: ```ts type UserId = string & { readonly __brand: "UserId" } ``` - Construct via a factory function that runs the validation. Don't cast. ## Generics - Single-letter generics for simple cases (`T`, `K`, `V`). Descriptive names for complex ones (`TUser`, `TKey`). - Constrain generics with `extends` — unbounded `T` is rarely what you want. - Avoid generics that exist only to "remember" a type. If the type is the same in and out, you don't need a generic. ## Errors - Throw `Error` subclasses with a `name`. Don't throw strings. - `try/catch` with `useUnknownInCatchVariables` forces you to narrow before reading the error. Use `if (err instanceof MyError)`. - Don't return errors via union types from unrelated functions. Pick a layer where Result types make sense and stay there. ## Don't - Don't use `Object`, `Function`, `{}` as types. They mean "anything". - Don't write `Array<X>` and `X[]` mixed in the same file. Pick one. - Don't use TypeScript enums. Use string-literal unions or `as const` objects. - Don't write barrel files that re-export everything — they break tree-shaking.Node.js + Express + TypeScript
# CLAUDE.md — Node.js + Express + TypeScript ## Setup - Node 20+ (LTS). Express 4 (LTS) — Express 5 if you can adopt the breaking changes. - ESM only. `"type": "module"`. Use `tsx` in dev, `tsc` for production builds. - Lock the major version of every prod dependency. `npm audit` in CI. ## Project layout ``` src/ app.ts # buildApp(): Express server.ts # entry point — reads env, starts listening routes/ users.ts controllers/ services/ repositories/ middleware/ lib/ errors.ts # custom error classes test/ ``` - `app.ts` builds and returns the Express instance. `server.ts` is the only file that calls `.listen()`. - One router per resource. Routers compose in `app.ts`. ## Middleware - Order matters: parse body → log → auth → routes → error handler. - Use **helmet** for default security headers. - Use **cors** with an allowlist — never `origin: "*"` in production. - Body parser via `express.json({ limit: "1mb" })`. Reject huge bodies at the edge. ## Async handlers - All handlers are `async`. Use `express-async-errors` (or wrap in a `asyncHandler` helper) so thrown errors hit the error middleware. - Don't write `try/catch` in every handler — push errors to the centralized error handler. ## Error handling - One central error handler at the bottom of the middleware stack: ```ts app.use((err, req, res, _next) => { if (err instanceof NotFoundError) return res.status(404).json({ error: err.message }) if (err instanceof ValidationError) return res.status(400).json({ error: err.message, details: err.details }) log.error({ err }, "unhandled") res.status(500).json({ error: "internal" }) }) ``` - Custom errors extend `Error` with a discriminator. Don't string-match error messages. ## Validation - Validate every request body, query, and param with **Zod** at the controller boundary. - Don't trust types — TypeScript doesn't validate runtime input. Zod does. ## Logging - **pino** with `pino-http` for request logs. Bind `req.id` (use `nanoid` or `crypto.randomUUID`) to every log line. - Don't `console.log` in handlers. ## Configuration - One `env.ts` with Zod parsing at boot. - Crash early on missing/invalid env vars — never default-fall-through to a value that "kinda works". ## Don't - Don't ship without `helmet`, `cors`, and rate limiting. - Don't put DB calls in controllers. Service → repository → DB. - Don't write callback-style code. `async/await` everywhere. - Don't use `req.params`/`req.body` without validating first. The types lie.Next.js + Server Actions + Shadcn UI
# CLAUDE.md — Next.js + Server Actions + Shadcn UI ## Forms & mutations - Use Server Actions for every form. The client form component imports the action and passes it to `<form action={...}>`. - Validate input inside the action with Zod. Reject invalid input before touching the database. - Return a discriminated `{ ok: true, data } | { ok: false, error }` from every action. The client renders state from this — never throws. - Use the `useActionState` (or `useFormState`) hook to keep server-returned errors next to the form fields. - For optimistic UI, use `useOptimistic` — don't manually duplicate state. ## Shadcn UI - Components live in `components/ui/`. Generated with `npx shadcn add <component>` — keep them under your control. - Compose from primitives: never reach for a "kitchen sink" library on top of Shadcn. - For variants, extend the existing `cva` class string — don't fork the file. - Re-export Shadcn primitives from a single barrel only if you also wrap them. Otherwise import directly from `components/ui/<file>`. - Shadcn components are client components by default. If a primitive is purely presentational, copy it into a server component file and drop the `'use client'` directive. ## Validation - One Zod schema per form, exported from `lib/validators/<feature>.ts`. - Infer types: `type FormInput = z.infer<typeof formSchema>`. Never declare the type by hand. - Reuse the schema on the client (resolver for `react-hook-form`) and the server (action input check). Don't duplicate. ## Patterns - Loading states: use the `pending` flag from `useActionState` — don't track it with `useState`. - After a successful mutation, call `revalidatePath` or `revalidateTag` from the action — not `router.refresh()` from the client. - Handle redirects with `redirect()` *inside* the action. The client never decides where to go after a mutation. ## Don't - Don't wrap a Server Action in a fetch from a client. Pass it to `<form action>` or call it directly via `useTransition`. - Don't return raw `Error` objects — they don't serialize. Return shape-checked plain objects. - Don't put business logic in form components. Keep the form thin; logic lives in the action. - Don't mix Shadcn with another UI kit. Pick one design system and commit.React Hooks Best Practices
# CLAUDE.md — React Hooks Best Practices ## Rules of Hooks - Call hooks at the top level of a function component or another hook. Never inside loops, conditions, or after early returns. - Hook names start with `use`. ESLint enforces this and it's load-bearing for the linter — don't rename around it. ## useState - Initialize with the value, not a getter, unless construction is expensive: `useState(() => expensiveInit())`. - One state per concern. Don't combine unrelated state into one big object. - For derived state, compute during render. Don't sync via `useEffect`. ## useEffect — the last resort - Effects are for synchronizing with external systems (DOM, network, timers). Not for "running code when X changes". - If the body of an effect could be an event handler, it should be. - Always specify the dep array. Empty `[]` only when you truly mean "once" and you're not using any value from the render. - Always return a cleanup function for subscriptions, intervals, observers. - Two effects, two responsibilities — never combine unrelated work into one effect. ## useRef - For values that need to persist across renders without causing re-renders. - For DOM refs: `useRef<HTMLInputElement>(null)`. Type the element narrowly. - Mutating `ref.current` doesn't re-render. If you need a re-render, use state. ## useMemo / useCallback - Default to *not* using them. They have a real cost — memoization itself takes work. - Use them when: - A value is passed to a memoized child and identity matters - A computation is provably expensive (you've measured) - Don't memoize primitive values. Don't memoize unstable inputs. ## Custom hooks - A hook is a function that calls other hooks. Anything that doesn't is a utility — keep it as a plain function. - One hook, one purpose. Compose smaller hooks; don't write a 200-line super-hook. - Return tuples for two values where order is obvious (`[value, setValue]`). Return objects when you have 3+ named values. ## useReducer - Reach for `useReducer` when state transitions are complex or multiple events update overlapping state. - Reducers must be pure. No fetches, no side effects. - Action types are string literals — type them as a discriminated union. ## Don't - Don't use `useEffect` for data fetching when a server framework or query library is available. - Don't read state values inside an effect via stale closures — add them to deps or use a ref. - Don't create new objects/arrays in render and pass them to memoized children. Memoize them. - Don't disable `react-hooks/exhaustive-deps`. Fix the dependency, don't hide the warning.TypeScript + Zod Validation
# CLAUDE.md — TypeScript + Zod Validation ## Where to validate - Validate at every **boundary**: HTTP request, HTTP response (third-party), DB row → object, env vars, message bus. - Don't validate inside business logic — by then, types are guaranteed. - One schema per shape. Co-locate with the code that consumes it. ## Schema basics - Schemas live in `lib/validators/<feature>.ts`. - Infer types from schemas — never declare types and schemas separately: ```ts export const UserSchema = z.object({ id: z.string().uuid(), email: z.string().email(), role: z.enum(["admin", "member"]), }) export type User = z.infer<typeof UserSchema> ``` - One source of truth: schema → type. If you find yourself writing the type by hand, you're probably duplicating. ## Parsing vs safeParsing - Use `.parse()` when invalid input is unrecoverable (env vars at boot, internal contracts). - Use `.safeParse()` at HTTP boundaries — return a typed error response on failure. Don't let zod throw across an HTTP handler. ## Composition - Build complex schemas from smaller ones: ```ts const Address = z.object({ street: z.string(), city: z.string() }) const User = z.object({ ..., address: Address }) ``` - Reuse schemas across requests/responses with `.pick`, `.omit`, `.partial`, `.required`. Don't redeclare related shapes. ## Coercion & transforms - `z.coerce.number()` for query strings (everything's a string in URLs). - `.transform(...)` for derived shapes — but the input and output types should both be intentional. - `.refine(...)` for cross-field rules. The error path goes in the second arg. ## Error messages - Set `errorMap` once at module scope to localize / standardize messages. - For form validation, use `z.flatten(error)` and feed to your form library. Don't write custom serializers. ## Performance - Compile schemas once at module scope. Don't construct them inside request handlers. - For large unions, prefer discriminated unions (`z.discriminatedUnion("type", [...])`) — they're orders of magnitude faster than plain `z.union`. ## Env vars - One `env.ts` parses `process.env` at boot: ```ts const Env = z.object({ DATABASE_URL: z.string().url(), SECRET_KEY: z.string().min(32), NODE_ENV: z.enum(["development", "production", "test"]), }) export const env = Env.parse(process.env) ``` - Crash on invalid env. Don't run with bad config. ## Don't - Don't use Zod for runtime checks of internal data after parsing once. The type is the guarantee. - Don't catch zod errors and rethrow as generic `Error` — you lose the rich `.errors` structure. - Don't define the type *and* the schema. The schema generates the type. - Don't validate the same data twice — once at the boundary, once internally. That's a smell that the boundary moved.Next.js + Supabase Auth
# CLAUDE.md — Next.js + Supabase Auth ## Setup - Use `@supabase/ssr` for the App Router. Do not use the legacy `@supabase/auth-helpers-nextjs`. - Create three Supabase clients, each in its own file: - `lib/supabase/server.ts` — for Server Components, route handlers, and Server Actions - `lib/supabase/client.ts` — for Client Components - `lib/supabase/middleware.ts` — for the middleware that refreshes session cookies ## Sessions - Run the middleware on every request. It calls `supabase.auth.getUser()` and refreshes the auth cookies. - In Server Components, always call `supabase.auth.getUser()` — never `getSession()`. `getSession` reads cookies without revalidating, so it's spoofable. - Trust `getUser()` only on the server. On the client, treat the user object as a hint and verify with the server before any sensitive UI. ## RLS (Row Level Security) - Enable RLS on every table. No exceptions. - Write `policy` blocks for each access pattern: `select`, `insert`, `update`, `delete`. Don't combine them with `using true`. - Reference `auth.uid()` in policies. Indexes on `user_id` columns are not optional. - Test policies with the SQL editor's "Run as user" feature before merging. ## Server Actions / Route Handlers - Auth checks happen at the top of every action. Pattern: ```ts const { data: { user } } = await supabase.auth.getUser() if (!user) return { ok: false, error: 'unauthorized' } ``` - Don't pass user IDs from the client. Read them from the authenticated session inside the action. ## Cookies & redirects - Sign-in / sign-out actions must call `redirect()` after the auth call so cookies flush. - Use `revalidatePath('/', 'layout')` after sign-in/out to drop cached user-dependent renders. ## Don't - Don't expose the service-role key to the browser or in any code path that runs in a Client Component. - Don't disable RLS to "fix" a query — write a policy. - Don't store auth tokens in `localStorage`. The Supabase SSR client uses cookies. - Don't use `next/router` for auth redirects in App Router code — use `redirect()` from `next/navigation`.React + TanStack Query
# CLAUDE.md — React + TanStack Query ## Setup - One `QueryClient` per app, instantiated above `<QueryClientProvider>`. - Add `<ReactQueryDevtools>` in dev only — gate with `import.meta.env.DEV` or `process.env.NODE_ENV`. - Default options live on the `QueryClient` constructor: `staleTime`, `retry`, `refetchOnWindowFocus`. Don't repeat them per-hook unless overriding. ## Query keys - Keys are arrays. The first element is a stable string namespace; following elements are scoping params: - `['users']` — the list - `['users', userId]` — one user - `['users', userId, 'posts']` — nested resource - Define key factories per feature in `features/<feature>/queries.ts`: ```ts export const userKeys = { all: ['users'] as const, detail: (id: string) => [...userKeys.all, id] as const, } ``` - Never construct keys inline at call sites — invalidation will silently miss. ## Queries - One custom hook per query: `useUser(id)`, `useUserList()`. The hook owns the query function and key. - Always type the data via the query function's return type — don't type at the hook signature. - Suspense queries (`useSuspenseQuery`) for data that's required to render the page; standard `useQuery` for optional data. - `select` to derive computed values without re-running the query. ## Mutations - One hook per mutation: `useUpdateUser()`. The hook owns the mutation function and post-mutation invalidation. - After a mutation, invalidate the query keys that depend on the changed data — don't refetch a single specific query when a key prefix would do. - For optimistic updates, use `onMutate` with `setQueryData` and roll back in `onError` using the snapshot. ## Caching - `staleTime` defaults to 0 — set per-feature to a value that makes sense (5 minutes for user-list, 0 for live data). - `gcTime` (formerly `cacheTime`) defaults to 5 minutes — increase for data the user revisits. - `refetchOnWindowFocus`: `false` for dashboards where freshness is < 5 min anyway; `true` for actively editable resources. ## Don't - Don't fetch in `useEffect` when TanStack Query exists. The cache, retry, and dedup logic are the point. - Don't share query keys across features. Namespace strictly. - Don't use a query for data that never changes — that's a constant. Import it directly. - Don't disable a query with `enabled: false` and then refetch with `refetch()` — pass the right deps to the key instead. - Don't put `useQuery` inside a conditional. The hook order is the cache identity.Node.js + Prisma + Postgres
# CLAUDE.md — Node.js + Prisma + Postgres ## Schema - `prisma/schema.prisma` is the source of truth. Every change goes through `prisma migrate dev`. - Datasource: Postgres. `provider = "postgresql"`. - For Vercel/serverless, set both `url` and `directUrl`: ```prisma datasource db { provider = "postgresql" url = env("DATABASE_URL") // pooled (pgbouncer) directUrl = env("DIRECT_URL") // direct (for migrations) } ``` ## Singleton client - One Prisma client per process. Stash on `globalThis` in dev to survive HMR: ```ts const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient } export const db = globalForPrisma.prisma ?? new PrismaClient({ log: ["error"] }) if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db ``` - `db.$connect()` is lazy; you usually don't need to call it explicitly. ## Migrations - Local: `prisma migrate dev --name <change>`. Reviewed and committed. - CI: `prisma migrate deploy`. Idempotent; safe to run on every deploy. - Never edit a merged migration. Add a new one to fix forward. - For data-only migrations, write a script that runs separately — don't mix DDL and large data updates. ## Queries - `select` to narrow returns. Default returns all scalars — wasteful and leaks fields. - `include` for related records. Pick `select` over `include` when you only need a few fields of the relation. - `findUnique` for primary-key/unique lookups. `findFirst` only when you need ordering or filters. - Cursor pagination over `skip`+`take` for large tables. ## Transactions - Use `db.$transaction` for multi-step writes. Pass an array of promises (sequential) or a callback (interactive): ```ts await db.$transaction(async (tx) => { const order = await tx.order.create({ data }) await tx.audit.create({ data: { orderId: order.id, action: "create" } }) }) ``` - Set `timeout` on long transactions to avoid holding locks. ## Connection pooling - For serverless, **always** front Prisma with pgbouncer (transaction mode) or use Prisma Accelerate. - `connection_limit` in the pooled URL controls per-instance pool size. Multiply by replicas to size the DB. - Direct connections only for migrations and admin scripts. ## Performance - Add an index for every `where` field you use in production. Prisma doesn't infer them — declare with `@@index`. - `EXPLAIN ANALYZE` on slow queries. Prisma generates the SQL; you can paste it into psql. - Batch reads: `findMany` with an `in` clause beats N round-trips. ## Don't - Don't share a Prisma client across short-lived serverless functions without a connection pooler — you'll exhaust the DB. - Don't return Prisma models directly from public APIs. Map to a DTO so internal field changes don't break consumers. - Don't use `db.$queryRawUnsafe`. The safe variants prevent SQL injection. - Don't run migrations from app boot in multi-replica deploys. Race conditions silently corrupt schemas.React Component Architecture
# CLAUDE.md — React Component Architecture ## Component types - **UI primitives** — `<Button>`, `<Input>`, `<Card>`. Live in `components/ui/`. No app logic, no data fetching, no domain knowledge. - **Feature components** — `<UserProfile>`, `<InvoiceList>`. Live in `features/<feature>/`. Compose primitives, own feature logic. - **Layouts / providers** — `<AppLayout>`, `<ThemeProvider>`. Live in `app/` or `layouts/`. Mostly structural. ## Composition over configuration - Prefer `children` and slot props over a long flat prop list. A component with 12 props is asking to be split. - Compound components for related parts: `<Tabs>`, `<Tabs.List>`, `<Tabs.Trigger>`, `<Tabs.Panel>`. Share state via context internal to the parent. - Render-props or hook patterns when the consumer needs to customize the rendering, not just the data. ## Prop design - Required props first, optional props after. - Boolean props default to `false`. Naming is positive: `disabled`, not `notDisabled`. - Avoid `is*` and `has*` for the same concept. Pick one (`isLoading`, `hasError`). - Use discriminated unions for mutually exclusive prop sets (`{ kind: 'text', value } | { kind: 'icon', icon }`). - Spread the rest of native HTML props (`...rest`) to the underlying element so consumers can pass `aria-*`, `data-*`, etc. ## Folder layout ``` features/users/ components/ UserCard.tsx UserAvatar.tsx hooks/ useUser.ts api.ts types.ts index.ts // public surface ``` - The `index.ts` is the only file other features should import from. - Internal files importing each other use relative paths; cross-feature imports use the alias (`@/features/users`). ## Splitting components - Split when: - The component has more than one reason to change - State and rendering are independent - You'd want to test parts separately - Don't split for line count alone. A 200-line cohesive component beats four 50-line confused ones. ## Performance - Memoize at boundaries (`React.memo`) only after profiling. Most components don't need it. - Big lists: use `react-window` or `@tanstack/react-virtual` instead of rendering thousands of nodes. - Code-split at the route level with `React.lazy`. Don't lazy-load primitives. ## Don't - Don't pass JSX as a prop just to "skip" rendering — use `<Suspense>` or conditional rendering. - Don't barrel-export every component from `components/index.ts`. Big barrels break tree-shaking. - Don't put feature logic into UI primitives. The primitive should work in any app. - Don't reach into a child's internals via refs. If you need to, redesign the API.Next.js + Tailwind + Prisma Stack
# CLAUDE.md — Next.js + Tailwind + Prisma ## Project layout - `app/` — routes - `components/` — shared components (server-first) - `lib/db.ts` — singleton Prisma client (see below) - `lib/actions/<feature>.ts` — Server Actions - `prisma/schema.prisma` — single source of truth for the data model ## Prisma client - Export a singleton from `lib/db.ts` to avoid exhausting connection pools in dev: ```ts import { PrismaClient } from '@prisma/client' const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient } export const db = globalForPrisma.prisma ?? new PrismaClient() if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db ``` - Never instantiate `new PrismaClient()` outside this file. - Run `prisma generate` on every schema change. Add it to `postinstall`. ## Data access - Query Prisma in Server Components and Server Actions only. Don't pass the Prisma client to the client. - Use `select` to project only the fields you need. Don't return entire rows by default. - Wrap multi-step writes in `prisma.$transaction`. If a write fails halfway, the rollback must be automatic. - For pagination, use cursor-based pagination (`cursor`, `take`) — not `skip` + `take`. ## Migrations - `prisma migrate dev --name <change>` for local schema changes; `prisma migrate deploy` in CI. - Never edit a migration file after merging. Generate a new migration to fix forward. - Seed data lives in `prisma/seed.ts`, runnable via `prisma db seed`. ## Tailwind - Use semantic spacing scale (`p-4`, `gap-3`) — no magic pixel values via `[12px]` unless absolutely required. - Component variants: use `cva` from `class-variance-authority`, not nested ternaries inside `className`. - Mobile-first: write base styles, then `sm:`, `md:`, `lg:`. Don't write `lg:` styles before base. ## Environment - `DATABASE_URL` and `DIRECT_URL` (for migrations) live in `.env`. Never commit `.env`. - For pooled connections (Vercel, etc.), set `DATABASE_URL` to the pooled URL and `DIRECT_URL` to the direct one in `prisma/schema.prisma`. ## Don't - Don't query Prisma from a Client Component or `useEffect`. Move it to the server. - Don't use `findFirst` when `findUnique` would do — uniqueness is a hint to the planner. - Don't disable type generation to ship. - Don't leak full DB error messages to the client.TypeScript Node.js Backend
# CLAUDE.md — TypeScript Node.js Backend ## Stack defaults - Node 20+ (LTS). Pin major version in `engines`. - Type-stripping: **tsx** for dev, **tsc** for production builds (`outDir: dist`). - ES modules only (`"type": "module"` in `package.json`). No CommonJS in new code. - Package manager: pnpm or npm. Lock file is committed. ## Project layout ``` src/ app.ts # express/fastify/hono app factory server.ts # bootstrap (reads env, starts listening) routes/ services/ repositories/ lib/ types/ test/ dist/ # gitignored tsconfig.json ``` - Separate `app.ts` (factory) from `server.ts` (entry point) so tests can import the app without binding ports. ## tsconfig ```json { "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true, "noUncheckedIndexedAccess": true, "verbatimModuleSyntax": true, "outDir": "dist", "rootDir": "src" }, "include": ["src/**/*"] } ``` - ES module imports use the **full filename** including `.js` (compiled) or rely on `tsx` which handles `.ts` extensions. ## Configuration - One `env.ts` with Zod parsing at boot. Crash early on invalid env. - Read `NODE_ENV` once. Branch in `env.ts`, not throughout the app. - Don't use dotenv at the source — let the orchestrator inject env. `dotenv` only in dev. ## Logging - **pino** — fast, structured, the right answer for production. - Log JSON to stdout. Let the orchestrator collect. - One logger per module: `const log = pino({ name: "users" })`. ## Errors - Custom errors extend `Error` with a `name` and a `code`: ```ts export class NotFoundError extends Error { name = "NotFoundError" constructor(public resource: string, public id: string) { super(`${resource} ${id} not found`) } } ``` - Map domain errors to HTTP statuses at the framework layer. Don't `throw new HttpError(404)` from a service. ## Process lifecycle - Handle `SIGTERM`. Drain in-flight requests, close DB pools, exit clean. - Don't `process.exit(0)` from a request handler. - Uncaught exceptions / unhandled rejections: log + exit. Let the orchestrator restart. ## Testing - **Vitest** or **Node's built-in test runner**. Fast and minimal. - Unit tests for services. Integration tests for routes against a real DB. - Snapshot tests for response shapes — they're cheap insurance. ## Don't - Don't use `require()` in TypeScript. Use `import`. - Don't ship `console.log` in production code. Use the structured logger. - Don't put SQL in route handlers. Repository → service → route. - Don't depend on Node-specific globals (`process`, `Buffer`) in code shared with the browser.Next.js SEO + Metadata Rules
# CLAUDE.md — Next.js SEO + Metadata ## Metadata API - Every page exports `metadata` (static) or `generateMetadata` (dynamic). No exceptions. - Always set: `title`, `description`, `alternates.canonical`, `openGraph`, `twitter`, `robots`. - Title pattern: `${PageName} | ${SiteName}`. Set the suffix once via `metadata.title.template` in the root layout. - Description: 140–160 characters. Avoid keyword stuffing — write a real sentence. ## Canonical URLs - Set `alternates.canonical` to the absolute URL on every indexable page. - For paginated routes, the canonical should point to the *current* page, not page 1. Don't auto-rewrite to page 1. - Trailing slashes: pick one and stick with it via `next.config.ts` `trailingSlash`. ## Sitemap & robots - `app/sitemap.ts` exports a function returning `MetadataRoute.Sitemap`. Generate from your data — never hand-maintain. - `app/robots.ts` exports a function returning `MetadataRoute.Robots`. Reference the sitemap. - Exclude staging, admin, and search-result pages with `disallow`. Don't rely on auth alone. ## OG images - One `opengraph-image.tsx` per route segment. Use `next/og`'s `ImageResponse`. - Size: 1200×630. `runtime: 'nodejs'` if you read from the filesystem; otherwise `'edge'` is fine. - Pre-render dynamic OG images via `generateStaticParams` so they're cached at the CDN. ## Structured data (JSON-LD) - Add a `<script type="application/ld+json">` per page where it applies — `Article`, `Product`, `BreadcrumbList`, `FAQPage`, `SoftwareApplication`. - Use `dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}` — don't render JSON-LD as a child string. - Validate with Google's Rich Results Test before claiming the page is "done". ## Performance signals - Ship `priority` only on the LCP image (one per page). - `next/font` for fonts — never `<link>` to Google Fonts. - Avoid client components on static content. Server Components ship zero JS. ## Internal linking - Use `<Link>` for same-origin navigation. Never `<a href>`. - Crawlers follow `<Link>`. They do *not* follow JavaScript-only navigation. ## Don't - Don't dynamically generate canonical URLs on the client. - Don't `noindex` pages by accident — review your `robots` field per page. - Don't ship a single shared OG image when you can ship per-page images. - Don't write meta tags by hand with `<meta>` in your JSX. Use the Metadata API.React + Redux Toolkit
# CLAUDE.md — React + Redux Toolkit ## When to use Redux - Use Redux Toolkit (RTK) when state is shared across many disconnected parts of the app, or when actions cause coordinated changes. - For server cache, use RTK Query (or TanStack Query). Don't keep server data in slices unless you have a specific reason. - For local component state, use `useState`/`useReducer`. Don't promote everything to global. ## Slices - One slice per domain. File: `src/features/<domain>/<domain>Slice.ts`. - Use `createSlice`. Reducers are written as if mutating — Immer handles immutability. - Co-locate selectors in the same file. Export `selectX` functions; never read state shape from components. - Action creators are auto-generated. Don't write them by hand. ## Store - Define the store once in `src/app/store.ts`: ```ts export const store = configureStore({ reducer: { users: usersReducer, ... } }) export type RootState = ReturnType<typeof store.getState> export type AppDispatch = typeof store.dispatch ``` - Wrap `useDispatch` and `useSelector` once with the typed versions: ```ts export const useAppDispatch: () => AppDispatch = useDispatch export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector ``` - Components import the typed hooks, never the raw `useDispatch`. ## Async with createAsyncThunk - Use `createAsyncThunk` only for actions that touch async work *and* need to be tracked in slice state (loading flag, error). - For simple data fetching, prefer RTK Query. - Three reducers per thunk: `pending`, `fulfilled`, `rejected`. Always handle `rejected` — never silently swallow. ## RTK Query - One `api` slice per backend. `createApi({ baseQuery, endpoints })`. - Tag-based invalidation: each query provides tags, each mutation invalidates them. Don't manually `refetch`. - Co-locate endpoints with the feature. Re-export hooks from a feature index file. ## Selectors - Use `createSelector` (Reselect, re-exported by RTK) for derived data. - Inputs are simple state-getter selectors; the result function does the computation. - Don't pass new objects/arrays through `useSelector` without `createSelector` — you'll re-render every state change. ## Don't - Don't mutate state outside slice reducers. Immer only works inside `createSlice`. - Don't dispatch from inside reducers. Reducers are pure. - Don't put non-serializable values (Maps, Sets, class instances, Promises) in state. - Don't store the same data in two slices. Pick the owner; let others read.Node.js + NestJS
# CLAUDE.md — Node.js + NestJS ## Mental model - NestJS is opinionated DI + decorator-driven HTTP/microservice framework on top of Express or Fastify. - Build for **modules**: a feature is a module that imports its dependencies and exports its public surface. - Don't fight the conventions — when in doubt, do what the docs do. ## Module structure ``` src/users/ users.module.ts users.controller.ts users.service.ts users.repository.ts dto/ create-user.dto.ts update-user.dto.ts entities/ user.entity.ts users.controller.spec.ts users.service.spec.ts ``` - One module per domain. Modules import other modules to use their providers — never reach into another module's `service` directly. - Public surface = whatever the module `exports`. Keep it small. ## Providers (services) - Annotate with `@Injectable()`. Constructor-inject dependencies — never `new` them. - Services hold business logic. Controllers hold parsing and shape; repositories hold data access. - Don't put HTTP concepts (status codes, request objects) in services. ## DTOs & validation - One DTO per request shape. `class-validator` + `class-transformer` decorators on every field. - Enable the global validation pipe in `main.ts`: ```ts app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true })) ``` - `whitelist: true` strips unknown properties; `forbidNonWhitelisted` rejects them with 400. ## Controllers - Thin. Decorators declare HTTP route, status code, response type. Body delegates to a service. - `@HttpCode(204)` for no-content responses. Don't return `void` and let Nest pick. - Use `@UseGuards(...)` for auth, `@UsePipes(...)` for per-route validation overrides. ## Pipes, guards, interceptors - **Pipes** transform/validate. Use Nest's built-ins (`ValidationPipe`, `ParseUUIDPipe`) before custom ones. - **Guards** authorize. One guard per concern. - **Interceptors** wrap responses (logging, caching, transforms). Don't use them for auth — use guards. ## Configuration - `@nestjs/config` with a Joi or Zod schema. Validate at boot. - Inject `ConfigService` into providers; don't read `process.env` directly. ## Testing - Unit-test services by constructing them with mocked deps. The Nest testing module is for integration tests, not units. - For controller integration, use `Test.createTestingModule(...)` with overrides for external systems. - Use **supertest** for full e2e tests against the bootstrapped app. ## Don't - Don't bypass DI by importing a class and calling `new`. - Don't put DB queries in controllers. - Don't write circular module imports. Refactor — they signal a bad boundary. - Don't expose entities directly in responses. Map to a Response DTO so DB renames don't break clients.TypeScript Monorepo (Turborepo)
# CLAUDE.md — TypeScript Monorepo (Turborepo) ## Layout ``` apps/ web/ # the Next.js / React / etc. app api/ # backend marketing/ packages/ ui/ # shared React components config/ # tsconfig, eslint, prettier presets utils/ # framework-agnostic utilities types/ # shared type definitions turbo.json package.json # workspaces root pnpm-workspace.yaml # if pnpm ``` - `apps/*` are deployable. `packages/*` are internal libraries. - Each package has its own `package.json`, `tsconfig.json`, build scripts. ## Package manager - **pnpm** is the default. It saves disk and enforces strict dep boundaries. - `pnpm-workspace.yaml`: ```yaml packages: - "apps/*" - "packages/*" ``` - Add `"workspace:*"` for internal package versions in dependent `package.json`s. ## TypeScript config - One base `tsconfig.json` in `packages/config/typescript`. Apps and packages extend it: ```json { "extends": "@repo/config/typescript/base.json", "include": ["src"] } ``` - Use **TypeScript project references** for incremental builds across packages. `tsc --build` walks the graph. - Each package emits to its own `dist/`. Source-only consumers read from `src/` via the `exports` map. ## Turbo pipelines - `turbo.json` declares task dependencies, inputs, outputs, and cache rules: ```json { "tasks": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] }, "lint": { "dependsOn": ["^build"] }, "test": { "dependsOn": ["^build"], "outputs": ["coverage/**"] } } } ``` - `^build` means "build my dependencies first." Get this right and CI flies. - Outputs are what's cacheable. Inputs default to all package files; narrow them when relevant. ## Caching - Local cache by default. **Remote cache** for CI: `turbo login` + `turbo link`, or self-hosted. - Cache misses on `node_modules` mean your `inputs` are too broad. - Don't cache flaky tests. Mark them `cache: false`. ## Internal package consumption - Use `workspace:*` everywhere — never relative paths into other packages. - Re-export the public surface from `package.json#exports`. Don't let consumers reach into `dist/internal/`. - For server-only code, mark with `"type": "module"` or split into separate sub-paths. ## Linting & formatting - One ESLint config in `packages/config/eslint/`. Apps extend it. - Ban cross-package imports that aren't declared as deps via `eslint-plugin-import`. - One Prettier config at the root. Don't fork per package. ## Don't - Don't put deployable apps inside `packages/` or libraries inside `apps/`. - Don't share devDependencies via the root `package.json` — each package declares what it actually uses. - Don't `tsc` from the root and pretend it's a build. Use Turbo's `build` task or it won't cache. - Don't depend on a package's internal files (`@repo/ui/dist/internal/x`). Always go through `exports`.Node.js + Fastify
# CLAUDE.md — Node.js + Fastify ## Why Fastify - Schema-first request validation (uses JSON Schema or **TypeBox** / **Zod via plugin**). - Faster than Express on JSON-heavy workloads. - Native plugin system with explicit encapsulation — better than ad-hoc Express middleware ordering. ## Setup - Node 20+. ESM (`"type": "module"`). - TypeScript with `@fastify/type-provider-typebox` (or `@fastify/type-provider-zod`) for schema-driven types. - Each plugin is a function that takes the `fastify` instance. ## Project layout ``` src/ app.ts # builds and decorates the fastify instance server.ts # entry point plugins/ # auth, db, swagger, etc. routes/ users.ts # one route file per resource schemas/ # TypeBox / Zod definitions services/ ``` ## Schemas - Every route declares `schema: { body, params, querystring, response }`. Don't ship a route without a response schema — it's a perf and correctness win. - TypeBox example: ```ts const UserParams = Type.Object({ id: Type.String({ format: "uuid" }) }) fastify.get("/users/:id", { schema: { params: UserParams } }, async (req) => { ... }) ``` - Validation runs at the boundary; the handler gets typed inputs. ## Plugins - One plugin per concern. `db.ts`, `auth.ts`, `metrics.ts`. - Use `fastify-plugin` for plugins that decorate the root instance — encapsulation breaks otherwise. - Lifecycle hooks (`onRequest`, `preHandler`, `onResponse`) are local to a plugin/encapsulated scope. Use that on purpose. ## Errors - `app.setErrorHandler(...)` once. Map known errors to status codes. - Throw `app.httpErrors.notFound("user")` etc. — Fastify ships an HTTP errors helper. - Validation errors are 400 by default; customize the message format with a `schemaErrorFormatter`. ## Logging - Fastify ships **pino**. Don't add a separate logger. - `request.log` for per-request context. `app.log` for app-level. - Add `app.addHook("onResponse", (req, reply) => req.log.info({ status: reply.statusCode }))` if you want structured access logs. ## Auth - Use **@fastify/jwt** for JWT, **@fastify/oauth2** for OAuth flows. - Auth check via `preHandler` hook on a route or scope. Don't duplicate per route. ## Performance - Set `logger.level: "info"` in production; `"debug"` only when diagnosing. - Enable HTTP/2 only when you have a real reason. It's not a free win. - Use `fastify-compress` only after profiling — gzip CPU isn't free. ## Don't - Don't add Express-style `app.use(middleware)`. Use Fastify hooks. - Don't return raw entity objects without a response schema. Fastify's serializer can't optimize what it can't see. - Don't share state via globals. Decorate the instance: `app.decorate("db", db)`. - Don't call `app.listen` from `app.ts`. That belongs in `server.ts`.TypeScript React Component Types
# CLAUDE.md — TypeScript React Component Types ## Prop typing - Props are an `interface`, not a `type`, when other components might extend them. - Required props first. Optional props after. - Document non-obvious props with TSDoc — it shows up in editor hovers. - Avoid optional booleans for two-state things. `kind: "primary" | "secondary"` reads better than `isPrimary?: boolean` when you have more than one variant. ## Common patterns ```ts // Children: free-form type Props = { children: React.ReactNode } // Children: a single element type Props = { children: React.ReactElement } // Render prop type Props = { children: (state: State) => React.ReactNode } // Forwarded native props type Props = React.ComponentPropsWithoutRef<"button"> & { variant: "primary" | "ghost" } ``` - `ComponentPropsWithoutRef<"button">` over `ButtonHTMLAttributes` — picks up `data-*`, `aria-*`, and event handlers automatically. ## Discriminated unions for variants ```ts type Props = | { kind: "icon"; icon: React.ReactNode; label: string } | { kind: "label"; label: string } ``` - `kind` makes the variant explicit. `switch (props.kind)` exhausts cases. - Don't use `XOR` types or "if A is set, B can't be" comments. Encode it in the type. ## Event handlers - Type the event with the React-specific generic: ```ts onClick: (e: React.MouseEvent<HTMLButtonElement>) => void ``` - For form events, narrow to the element you actually use. - Don't type as `(e: any) => void` "for simplicity". The type cost is one extra word. ## Refs & polymorphic components - `forwardRef` for components that wrap a DOM node and need to expose the ref: ```ts const Button = React.forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => ...) ``` - For polymorphic components (`as` prop), the typing is intricate. Use a battle-tested pattern (e.g., from radix-ui or a small util like `react-polymorphic-types`) — don't reinvent. ## Generics in components - Components can be generic for list/select/table primitives: ```ts function Select<T>({ items, value, onChange }: { items: T[]; value: T; onChange: (v: T) => void }) { ... } ``` - Constrain generics with `extends` when you need a property: `<T extends { id: string }>`. ## Default values - Default props in destructuring: `{ variant = "primary" }: Props`. No `defaultProps`. - For boolean props, default to `false` and name positively. ## Don't - Don't type `children` as `JSX.Element` — too restrictive (excludes strings, fragments, null). - Don't use `React.FC` / `React.FunctionComponent`. It implies an unwanted `children` and complicates generics. - Don't pass through every prop as `{...props}` to `<div>`. Whitelist the props that make sense. - Don't widen prop types ("just in case"). The narrower, the better the contract.Node.js Microservice Patterns
# CLAUDE.md — Node.js Microservice Patterns ## Boundaries - One service per **bounded context**, not per table or per resource. - A service owns its data. No service reads another service's database directly — always through an API. - If two services constantly co-deploy and co-change, they're one service. ## Communication - **Sync** (HTTP/gRPC) for queries that need an immediate answer. - **Async** (events, queues) for state propagation — orders → inventory, user-created → email-welcome. - Default to async. Sync calls between services create cascade failures. ## Idempotency - Every mutation accepts an idempotency key. Same key = same response, even on retry. - Implement with a key→result table indexed on the key. TTL old keys. - Without idempotency, retries (yours or the client's) duplicate work. There will be retries. ## Events - Schema-first. Use **Avro**, **Protobuf**, or at least a **JSON Schema** registry. - Events are immutable facts: `OrderPlaced`, `PaymentSettled`. Past tense. - Emit on commit, not on intent. Outbox pattern: write the event row in the same DB transaction as the state change; a separate process publishes from the outbox. ## Failure handling - Retries are bounded: 3–5 attempts with exponential backoff and jitter. - After max retries, send to a **dead-letter queue**. Alert on DLQ growth. - Circuit breakers (`opossum` or hand-rolled) on every cross-service call. Fail fast when downstream is unhealthy. ## Observability - **Distributed tracing** with OpenTelemetry. Propagate `traceparent` across HTTP, queues, and async hops. - Structured logs with `trace_id` and `span_id` on every line. - Per-service metrics: requests/sec, p99 latency, error rate, queue depth. ## Versioning - API versioning in the URL or header. `v1` is forever; `v2` is additive at first. - Producer compatibility for events: only **add** fields, never remove or rename. Consumers tolerate unknown fields. ## Service mesh / gateway - An API gateway terminates TLS, does auth, and routes. Services trust the gateway's auth claims. - Mesh (Istio, Linkerd) for mutual TLS and traffic policies. Don't reinvent in app code. ## Local development - One docker-compose per service group. New devs `docker compose up`. - Stub external services with **wiremock** or **mockoon**. - Don't run all services on every laptop. Run the ones you change; talk to a shared dev environment for the rest. ## Don't - Don't share databases across services to "save effort". You're trading short-term convenience for long-term coupling. - Don't expose internal IDs across service boundaries. Use stable UUIDs. - Don't synchronously call N services to assemble a response. Aggregate via materialized views or a BFF. - Don't deploy services together as a unit. The whole point is independent rollout.
Have a CLAUDE.md template that works for you?
Send it in — we’ll credit you and publish it under the right tags.