The existing CLAUDE.md templates by use case cover 10 common project types. This guide goes further: each template here is annotated with why each section exists and what specific Claude behavior it prevents or enables. These are not minimal examples — they are production-ready starting points calibrated for real-world complexity.
Template 1: React SPA with Vite + TypeScript
What Claude gets wrong without this: Mixing Vite and CRA assumptions, using outdated hook patterns, ignoring the existing component architecture, adding unnecessary dependencies.
# Project
Customer-facing dashboard SPA. React 19, TypeScript strict mode, Vite 6.
State management: Zustand. Data fetching: TanStack Query v5.
## Commands
```bash
pnpm dev # Vite dev server on :5173
pnpm build # TypeScript check + Vite build
pnpm test # Vitest unit tests
pnpm test:ui # Vitest UI (browser-based test runner)
pnpm test:e2e # Playwright E2E tests
pnpm lint # ESLint
pnpm typecheck # tsc --noEmit
Project Structure
src/
features/ # feature-sliced architecture
auth/
components/ # feature-specific components
hooks/ # feature-specific hooks
store.ts # Zustand slice
api.ts # TanStack Query hooks
dashboard/
users/
shared/
components/ # shared UI primitives
hooks/ # shared hooks
lib/ # utilities, formatters
app/
router.tsx # React Router config
providers.tsx # Root providers
Anti-Patterns
- No prop drilling — use Zustand for cross-component state
- No
useEffectfor data fetching — use TanStack Query - No direct API calls in components — all API calls go through
features/*/api.ts - No
anytypes — useunknownand narrow, or define proper types - No barrel exports (
index.tsre-exporting everything) inshared/— import directly - No class components
- No
React.FCtype — use plain function declarations
State Management Rules
Zustand slices: one file per feature, exported as a named hook.
// Good
export const useAuthStore = create<AuthState>()((set) => ({
user: null,
setUser: (user) => set({ user }),
}))
// Never use zustand for server state — that's TanStack Query's job
Testing
Vitest + React Testing Library. Test behavior, not implementation.
# Run tests for a specific feature
pnpm test src/features/auth
# Run with coverage
pnpm test --coverage
Mock API calls at the network level with MSW — do not mock module imports for API functions.
Before Done
-
pnpm typecheckpasses (no new TS errors) -
pnpm lintpasses -
pnpm testpasses - No new prop drilling introduced
- No direct API calls in components
## Template 2: Full-Stack Next.js App
**What Claude gets wrong without this:** Mixing App Router and Pages Router patterns, incorrect server/client component boundaries, using client-side state for server data, ignoring streaming patterns.
```markdown
# Project
B2B SaaS product. Next.js 15 App Router, TypeScript, PostgreSQL via Drizzle ORM.
Authentication: Auth.js v5. Styling: Tailwind CSS 4.
## Commands
```bash
pnpm dev # Next.js dev server
pnpm build # production build
pnpm start # production server
pnpm test # Vitest unit/integration
pnpm test:e2e # Playwright E2E
pnpm db:push # push schema changes (dev only)
pnpm db:migrate # generate and run migrations (production)
pnpm db:studio # Drizzle Studio
React Server Components vs Client Components
Default to Server Components. Mark as Client Component only when:
- Using hooks (
useState,useEffect,useContext) - Using browser APIs
- Using event listeners
- Using third-party components that require client runtime
// Server Component (default, no directive needed)
export default async function UserList() {
const users = await db.select().from(usersTable)
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}
// Client Component (explicit 'use client' required)
'use client'
export function SearchInput() {
const [query, setQuery] = useState('')
return <input value={query} onChange={e => setQuery(e.target.value)} />
}
Data Fetching
- Fetch in Server Components — not in
useEffect - Use
fetchwith Next.js caching options, or query the DB directly - Client-side data: TanStack Query with server actions as mutation endpoints
- Route handlers (
app/api/) only for third-party webhook endpoints
Database
// Query pattern — always use transactions for multi-step operations
const result = await db.transaction(async (tx) => {
const user = await tx.insert(usersTable).values(data).returning()
await tx.insert(auditTable).values({ userId: user[0].id, action: 'create' })
return user[0]
})
Never use raw SQL. Schema changes go in db/schema.ts, then run pnpm db:migrate.
Off-Limits
pages/directory — App Router only, no Pages Routerapp/api/auth/— managed by Auth.js, do not modifydb/migrations/— auto-generated by Drizzle, do not edit
Anti-Patterns
- No
'use client'at the layout level unless necessary - No data fetching in client components (except optimistic updates)
- No server actions for reads — only for mutations
- No direct DB access from client components
- No environment variables without
NEXT_PUBLIC_prefix accessed client-side
## Template 3: Go REST API
**What Claude gets wrong without this:** Imperative error handling instead of Go idioms, mixing business logic into handlers, forgetting `context.Context` propagation, incorrect test table structure.
```markdown
# Project
REST API for [service description]. Go 1.23, Chi router, PostgreSQL via pgx.
Auth: JWT with RS256. Observability: OpenTelemetry → Grafana.
## Commands
```bash
go run ./cmd/api # run the server
go test ./... # all tests
go test ./... -race # with race detector (always run before PR)
go test -run TestUserService ./internal/users/
golangci-lint run # lint (config in .golangci.yml)
go generate ./... # run code generators (mockery, sqlc)
make migrate-up # run database migrations
make migrate-create name=xxx # create new migration
Project Structure
cmd/api/ # main package, only wiring
internal/
users/ # domain package
handler.go # HTTP handler (Chi)
service.go # business logic
repository.go # data access
model.go # domain types
shared/
middleware/
database/
config/
pkg/ # public packages (if any)
migrations/ # SQL migration files
Error Handling
All errors returned, never panicked (except in main for startup failures).
Error wrapping: fmt.Errorf("users: get by id: %w", err).
// Good
func (s *Service) GetUser(ctx context.Context, id int64) (*User, error) {
user, err := s.repo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("users service: get user: %w", err)
}
return user, nil
}
// Handler maps domain errors to HTTP status codes
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
user, err := h.service.GetUser(r.Context(), id)
if err != nil {
if errors.Is(err, ErrUserNotFound) {
http.Error(w, "not found", http.StatusNotFound)
return
}
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(user)
}
Context Propagation
Pass context.Context as the first argument to every function that does I/O. Never store contexts in structs.
Testing
Table-driven tests in _test.go alongside source. Use testify for assertions.
Integration tests: real DB (Docker), transaction rollback in each test.
func TestService_GetUser(t *testing.T) {
tests := []struct {
name string
id int64
want *User
wantErr error
}{
{"existing user", 1, &User{ID: 1, Name: "Test"}, nil},
{"not found", 999, nil, ErrUserNotFound},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := service.GetUser(context.Background(), tt.id)
assert.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})
}
}
Anti-Patterns
- No global state — use dependency injection through constructors
- No
init()functions — explicit initialization in main - No
panicin handlers — always return errors - No naked returns from functions longer than 5 lines
- Error messages: lowercase, no trailing period, descriptive
- Never
log.Fataloutside main
## Template 4: Nx Monorepo (TypeScript)
**What Claude gets wrong without this:** Running commands from the wrong directory, creating apps/libs in wrong locations, missing implicit dependencies in project graph.
```markdown
# Project
Nx monorepo with React apps and shared libraries. TypeScript throughout.
Apps: `web` (customer portal), `admin` (internal dashboard).
Libs: `ui` (design system), `data-access` (API clients), `util` (shared utils).
## Commands
```bash
# ALWAYS use nx commands, not raw package manager commands
nx run web:dev # dev server for web app
nx run admin:dev # dev server for admin app
nx run web:build # build web app
nx run-many -t build # build everything
nx run web:test # test web app
nx run-many -t test # test everything affected
nx affected -t test # test only affected projects
nx generate @nx/react:component MyComponent --project=web
nx generate @nx/js:library my-lib --directory=libs/my-lib
Boundaries (enforced by Nx)
apps/ can import from: libs/
libs/ui can import from: (nothing in this repo)
libs/data-access can import from: libs/util
libs/util can import from: (nothing in this repo)
Do not violate these import boundaries — Nx lint will catch it, but do not introduce the violation.
Off-Limits
nx.json— workspace-level Nx config, do not modifyproject.jsonin any project — targets/executors, do not modifytsconfig.base.json— path aliases, do not modify
New Code Location
- New React apps:
nx generate @nx/react:app— never create manually - New shared components: add to
libs/ui, not to an app’ssrc/ - New API clients: add to
libs/data-access - App-specific utilities:
apps/{app}/src/lib/
Anti-Patterns
- No imports across app boundaries (
webcannot import fromadmin) - No shared state between apps (each app has its own Zustand store)
- No
../../imports that cross project boundaries — use@scope/lib-namepaths
## Template 5: Open Source Library (TypeScript)
**What Claude gets wrong without this:** Breaking public APIs without version bumps, missing dual ESM/CJS exports, writing tests that test internals rather than public API.
```markdown
# Project
`@yourscope/library-name` — [one-line description].
Published to npm. Supports Node.js 20+ and browsers.
## Commands
```bash
pnpm build # tsup build (ESM + CJS + types)
pnpm test # Vitest
pnpm test:types # tsd — verify public type signatures
pnpm lint # ESLint + publint (package.json exports check)
pnpm size # bundlesize check
pnpm release # changeset version + npm publish (humans only)
Public API Rules
Public API = everything exported from src/index.ts.
Breaking changes require a major version bump. Breaking = any of:
- Removing an export
- Changing a function signature (parameters or return type)
- Changing a type’s required properties
- Changing default behavior
Additions are minor, fixes are patch. Use changesets: pnpm changeset.
Implementation Rules
Internal APIs: prefix with _ or use unexported names. Never rely on symbol names starting with _ being stable in the public API.
// Public (in src/index.ts exports)
export function createClient(options: ClientOptions): Client { ... }
// Internal (not exported from index.ts)
function _buildRequest(opts: BuildOptions): Request { ... }
Tree-Shaking
Every export must be individually tree-shakeable. Do not use object-wide imports.
// Good — each export is independent
export { feature1 } from './feature1'
export { feature2 } from './feature2'
// Bad — forces users to include everything
export * from './all-features'
Testing
Test the public API, not internals. If a test imports from anywhere other than src/index.ts, reconsider whether you are testing the right thing.
import { createClient } from '../src' // always from public entry
describe('createClient', () => {
it('creates a client with default options', () => {
const client = createClient({ endpoint: 'http://test' })
expect(client).toBeDefined()
})
})
Compatibility
Target: ES2020. Do not use features that require polyfills in Node.js 20+.
No bundler-specific features (no Vite/Webpack magic). Must work with node --experimental-vm-modules.
## Template 6: React Native / Expo App
**What Claude gets wrong without this:** Using web-only APIs, ignoring platform differences, missing Expo-specific patterns, wrong navigation structure.
```markdown
# Project
Expo 53 app (React Native 0.77). TypeScript. iOS and Android.
Navigation: Expo Router (file-based). State: Zustand. API: TanStack Query.
## Commands
```bash
npx expo start # Metro bundler (scan QR with Expo Go)
npx expo start --ios # iOS simulator
npx expo start --android # Android emulator
npx expo run:ios # native iOS build
npx expo run:android # native Android build
pnpm test # Jest with jest-expo
eas build --profile preview # EAS cloud build (preview)
Platform Differences
Always check behavior on both platforms. Common differences:
- Shadows: iOS
shadowOffset/shadowOpacity, Androidelevation - Text rendering: wrap all text in
<Text>, never bare strings - Keyboard:
KeyboardAvoidingViewwithbehavior="padding"on iOS,behavior="height"on Android
// Platform-specific styling
const styles = StyleSheet.create({
shadow: {
...Platform.select({
ios: { shadowColor: '#000', shadowOffset: {width: 0, height: 2}, shadowOpacity: 0.1 },
android: { elevation: 4 },
}),
},
})
Navigation (Expo Router)
File-based routing in app/. Each file = a screen.
app/
(tabs)/ # Tab navigator group
index.tsx # Home tab
profile.tsx # Profile tab
(auth)/
login.tsx
register.tsx
_layout.tsx # Root layout
Never use navigate() with hardcoded string paths. Use typed routes:
router.push('/(tabs)/profile') // good
router.push('/profile') // inconsistent — avoid
Anti-Patterns
- No web-only APIs: no
document,window,localStorage— useexpo-*equivalents - No inline styles for repeated elements — use
StyleSheet.create() - No
FlatListvsScrollViewconfusion: FlatList for long lists, ScrollView for short content - No async operations in render — use TanStack Query for data
- No direct
fetch— wrap in a query function with error handling
## Template 7: Chrome Extension (Manifest V3)
**What Claude gets wrong without this:** Using Manifest V2 APIs, mixing content scripts and service workers, not understanding the isolated world model.
```markdown
# Project
Chrome Extension (Manifest V3). TypeScript + React for popup/options pages.
Bundled with Vite + CRXJS plugin.
## Commands
```bash
pnpm dev # dev mode with HMR
pnpm build # production build to dist/
pnpm preview # test against a static server
Extension Architecture
src/
background/ # Service Worker (no DOM access)
index.ts
content/ # Content scripts (page DOM access, isolated JS world)
index.ts
popup/ # Popup UI (React app)
App.tsx
options/ # Options page (React app)
shared/ # Shared utilities (no browser-specific APIs)
manifest.json
Service Worker Rules
The background service worker has NO DOM access. It cannot use window, document, or localStorage. For storage, use chrome.storage.local or chrome.storage.sync.
// Good — use chrome.storage in service worker
await chrome.storage.local.set({ key: value })
const result = await chrome.storage.local.get('key')
// Bad — not available in service worker
localStorage.setItem('key', value) // undefined
Message Passing
Components communicate via chrome.runtime.sendMessage / chrome.tabs.sendMessage.
// Content script → background
chrome.runtime.sendMessage({ type: 'ANALYZE_PAGE', payload: { url } })
// Background listener
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'ANALYZE_PAGE') {
// handle
sendResponse({ status: 'ok' })
}
return true // keep channel open for async response
})
Permissions
Never request more permissions than needed. If a feature needs a new permission, note it explicitly — it requires manifest.json and Chrome Web Store review.
Anti-Patterns
- No
eval()— blocked by Manifest V3 CSP - No remote code execution — all scripts must be bundled
- No
XMLHttpRequestin service workers — usefetch - No DOM manipulation from service workers
- Never store sensitive data in
chrome.storagewithout encryption
## Template 8: CLI Tool (TypeScript/Node.js)
**What Claude gets wrong without this:** Using process.exit() incorrectly, not handling stdin/stdout properly, missing cross-platform path handling.
```markdown
# Project
CLI tool: `mycli` — [what it does in 5 words].
Node.js 22+. TypeScript. Single binary via pkg/esbuild.
## Commands
```bash
pnpm dev # tsx watch mode
pnpm build # esbuild bundle
node dist/index.js --help # test built output
pnpm test # Vitest
pnpm test:integration # integration tests (spawns the CLI)
CLI Design Principles
- Exit code 0: success. Exit code 1: user error. Exit code 2: internal error.
- All errors go to
stderr, output tostdout - Support
--jsonflag for machine-readable output - Support
NO_COLORenv variable
// Good error handling
try {
await doSomething()
} catch (err) {
console.error(`Error: ${err.message}`)
process.exit(1) // user-facing error
}
// Never throw in async without catching at top level
Output Formatting
Use chalk for colors (already installed). Check NO_COLOR before applying.
Use ora for spinners on long operations. Stop spinner before writing output.
Path Handling
Always use path.resolve() for file paths. Never concatenate paths with string interpolation.
// Good
const configPath = path.resolve(process.cwd(), '.myconfig.json')
// Bad
const configPath = process.cwd() + '/.myconfig.json'
Anti-Patterns
- No synchronous file reads for large files — use streams
- No hardcoded ANSI escape codes — use chalk
- No process.exit() in library code — only in the CLI entry point
- No global mutable state — pass config through function arguments
## Template 9: Infrastructure / Terraform
**What Claude gets wrong without this:** Incorrect resource lifecycle, missing variable validation, unsafe data source patterns, wrong module interface design.
```markdown
# Project
AWS infrastructure managed with Terraform 1.9+. State in S3, locking in DynamoDB.
Environments: dev, staging, prod. Separate state files per environment.
## Commands
```bash
# Always work from environment directory
cd environments/dev
terraform init
terraform plan
terraform apply
# Lint and format
terraform fmt -recursive
tflint
terrascan scan
# Module development
cd modules/vpc
terraform init
terraform test # Terraform test framework
File Structure
modules/ # reusable modules
vpc/
main.tf # resources
variables.tf # inputs
outputs.tf # outputs
versions.tf # provider requirements
environments/ # per-environment configurations
dev/
staging/
prod/
Safety Rules
Plan before apply — never terraform apply -auto-approve in staging or prod.
For any resource with prevent_destroy = false in staging/prod, add a comment explaining why it is not protected.
Never use count for resources that will need to be referenced by index — use for_each with a stable key instead.
Anti-Patterns
- No hardcoded region — use variable
- No
terraform destroywithout explicit human confirmation in PR - No data sources that can fail silently (
data.aws_amiwithout filters) - No hardcoded credentials — use IAM roles and assume role
tagsrequired on all resources:Environment,ManagedBy = "terraform",Project
Outputs
All modules must output their primary resource IDs. External consumers use outputs, never resource addresses.
## Template 10: Documentation Site (Astro/Docusaurus)
**What Claude gets wrong without this:** Breaking the content schema, adding non-existent components, wrong frontmatter, incorrect Markdown dialect.
```markdown
# Project
Documentation site built with Astro 5. Content in `src/content/docs/`.
Deployed on Cloudflare Pages. Search via Pagefind.
## Commands
```bash
pnpm dev # dev server on :4321
pnpm build # production build
pnpm preview # preview built site
Content Rules
All docs pages are Markdown files in src/content/docs/. Required frontmatter:
---
title: "Page Title" # required
description: "SEO description" # required
sidebar:
order: 10 # controls sidebar position (optional)
---
Do not add frontmatter fields not listed above — they will cause build errors.
Writing Style
- Technical, direct, no marketing language
- Code examples for every new concept
- Use GitHub Flavored Markdown, not MDX (no JSX in docs content)
- Images go in
src/assets/and are referenced as../../assets/image.png
Navigation
Navigation is auto-generated from directory structure. Do not edit astro.config.mjs navigation config — it is generated.
To add a new section: create a new directory in src/content/docs/.
To reorder: change the sidebar.order frontmatter value.
Anti-Patterns
- No HTML in Markdown content (except for tables — those are acceptable)
- No absolute URLs for internal links — use relative links
- No images wider than 1200px
- No modifying
src/components/without understanding the component system - No JavaScript in content files — content is Markdown only
## Choosing Your Template
These templates are starting points, not final configurations. Start with the closest match, delete sections that do not apply to your stack, and add specifics about your actual setup.
The templates that consistently produce the most improvement are those with strong anti-patterns sections and explicit before-done checklists. If you can only add two things to an existing CLAUDE.md, make them those.
For the reasoning behind what makes these patterns effective, see [CLAUDE.md best practices 2026](/blog/claude-md-best-practices-2026). For Python-specific patterns, see [CLAUDE.md for Python projects](/blog/claude-md-for-python-projects).