Unlike traditional AI assistants that require repeated context, Claude Code automatically loads project instructions at session start. This eliminates:
- ❌ Repeating project context every session
- ❌ Inconsistent AI behavior across sessions
- ❌ Manual copy-pasting of coding standards
- ❌ Context loss between terminal restarts
Impact Metrics (from Anthropic internal usage):
- 60% reduction in context-setting time
- 40% improvement in code consistency
- 3x faster developer onboarding
- 25% fewer debugging iterations
Core Philosophy
Persistent Context Over Ephemeral Prompts
Claude Code treats your CLAUDE.md as a permanent extension of its system prompt—not a one-time instruction. Every code generation, explanation, and refactoring references these rules.
2. Understanding the File System
File Locations & Priority Order
Claude Code searches for configuration files using a hierarchical scanning system:
Priority 1: User-Level Global (Personal Preferences)
~/.claude/CLAUDE.md
Priority 2: Project Root (Team-Shared)
./CLAUDE.md
or
./.claude/CLAUDE.md
Priority 3: Subdirectory (Module-Specific)
./packages/frontend/CLAUDE.md
./services/api/CLAUDE.md
Priority 4: Organization-Level (Enterprise Linux/Unix)
/etc/claude-code/CLAUDE.md
Loading Behavior & Merging
How Claude Reads Files:
- Starts at current working directory
- Recurses UP the file tree (child → parent → root)
- Combines all found files (more specific overrides more general)
- Loads on-demand for subdirectories when editing files in those paths
Example Scenario:
/home/user/projects/monorepo/
├── CLAUDE.md # Loaded: General monorepo rules
├── packages/
│ ├── frontend/
│ │ ├── CLAUDE.md # Loaded: When editing frontend files
│ │ └── src/App.tsx # ← Current file
│ └── backend/
│ └── CLAUDE.md # NOT loaded (different directory)
└── ~/.claude/CLAUDE.md # Loaded: Your personal preferences
Active context when editing App.tsx:
- Personal:
~/.claude/CLAUDE.md - Monorepo:
/home/user/projects/monorepo/CLAUDE.md - Frontend:
/home/user/projects/monorepo/packages/frontend/CLAUDE.md
Internal File Format
Format: Pure Markdown (.md)
Encoding: UTF-8
Line Endings: Unix (LF) or Windows (CRLF) both supported
Max Recommended Size: 50KB (~12,000 words)
Parsing Method: Full-text inclusion (no schema validation)
What Claude Does Internally:
# Simplified internal pseudocode
def load_claude_context(current_directory):
context = []
# Walk up directory tree
path = current_directory
while path != filesystem_root:
if exists(f"{path}/CLAUDE.md"):
context.append(read_file(f"{path}/CLAUDE.md"))
elif exists(f"{path}/.claude/CLAUDE.md"):
context.append(read_file(f"{path}/.claude/CLAUDE.md"))
path = parent_directory(path)
# Add global config
if exists("~/.claude/CLAUDE.md"):
context.append(read_file("~/.claude/CLAUDE.md"))
# Reverse (most specific first)
context.reverse()
# Combine into system prompt
return "\n\n".join(context)
File Permissions & Security
Recommended Permissions:
# Project CLAUDE.md (version controlled, team-shared)
chmod 644 CLAUDE.md
# rw-r--r-- (owner: read/write, group/others: read)
# Global CLAUDE.md (personal, private)
chmod 600 ~/.claude/CLAUDE.md
# rw------- (owner: read/write only)
Security Best Practices:
# ✅ DO: Version control project rules
git add CLAUDE.md
git commit -m "docs: add Claude Code project rules"
# ❌ DON'T: Commit secrets
# NEVER include API keys, passwords, or tokens in CLAUDE.md
# ✅ DO: Reference environment variables
echo "API_KEY: Use \$API_KEY environment variable" >> CLAUDE.md
# ❌ DON'T: Hardcode credentials
# Bad: API_KEY=sk_live_abc123xyz789
3. CLAUDE.md: The Core Configuration
The Canonical 8-Section Structure
Based on Anthropic's recommended format (from official docs and internal usage):
# [Project Name] - Claude Code Configuration
**Version**: 1.0.0
**Last Updated**: 2025-11-29
**Maintainer**: @username
**Review Cycle**: Quarterly
---
## Section 1: PROJECT OVERVIEW (50-100 words)
## Section 2: TECHNOLOGY STACK
## Section 3: PROJECT STRUCTURE
## Section 4: CODING STANDARDS
## Section 5: COMMON COMMANDS
## Section 6: TESTING REQUIREMENTS
## Section 7: KNOWN ISSUES & PITFALLS
## Section 8: IMPORTANT NOTES
Section 1: Project Overview
Purpose: Immediate orientation for Claude
Token Budget: 150-250 tokens
Update Frequency: On major architecture changes
## 1. PROJECT OVERVIEW
**Type**: Full-stack SaaS application
**Domain**: E-commerce platform for small businesses
**Primary Goal**: Handle 10,000 concurrent users with <100ms API response times
### Key Characteristics
- Multi-tenant architecture with row-level security
- Real-time inventory updates via WebSocket
- Payment processing through Stripe
- Mobile-first responsive design
### Non-Goals (What This Project Is NOT)
- NOT a marketplace (direct seller-to-buyer only)
- NOT supporting cryptocurrency payments
- NOT including social media features
Why This Format Works:
- ✅ Concise (Claude processes in <1 second)
- ✅ Disambiguates scope ("Non-Goals" prevents feature creep suggestions)
- ✅ Sets domain context (affects terminology and patterns)
Section 2: Technology Stack
Purpose: Establish exact versions and dependencies
Token Budget: 300-400 tokens
Update Frequency: On version upgrades or stack changes
## 2. TECHNOLOGY STACK
### Frontend
- **Framework**: Next.js 15.1.0 (App Router) [NOT Pages Router]
- **Language**: TypeScript 5.4.5 (strict mode enabled)
- **Styling**: Tailwind CSS 4.0.2
- **Component Library**: shadcn/ui 2.0 + Radix UI
- **State Management**:
- Server State: TanStack Query v5.17.1
- Client State: Zustand 4.5.0
- Form State: React Hook Form 7.5.0 + Zod 3.23.8
### Backend
- **Runtime**: Node.js 20.11.0 LTS
- **API Framework**: tRPC 11.0.0 (type-safe RPC)
- **Database**: PostgreSQL 16.1
- **ORM**: Drizzle ORM 0.35.0 [NOT Prisma]
- **Authentication**: NextAuth.js v5 (Auth.js)
- **Caching**: Redis 7.2 (via Upstash)
### DevOps & Tooling
- **Monorepo**: Turborepo 2.0.1
- **Package Manager**: pnpm 9.0.0 [NEVER use npm or yarn]
- **Testing**:
- Unit/Integration: Vitest 2.0.0
- E2E: Playwright 1.45.0
- Component: Testing Library
- **Linting**: ESLint 9.0 + TypeScript ESLint
- **Formatting**: Prettier 3.2.0
- **CI/CD**: GitHub Actions
- **Hosting**: Vercel (production), Railway (staging)
### Why These Versions Matter
- Next.js 15: Uses new `fetch` caching semantics (different from 14)
- Drizzle vs Prisma: Drizzle chosen for SQL-first approach and performance
- pnpm: Workspace protocol used extensively in monorepo
Critical Details:
| Detail Type | Why It Matters | Bad Example | Good Example |
|---|---|---|---|
| Version Numbers | APIs change between versions | "Next.js" | "Next.js 15.1.0 (App Router)" |
| Negative Specifications | Prevents wrong assumptions | "Use tRPC" | "tRPC 11.0 [NOT REST]" |
| Reasoning | Explains non-obvious choices | "Use Drizzle ORM" | "Drizzle for SQL-first approach" |
| Tool Alternatives | Clarifies exact command | "Install packages" | "pnpm install [NEVER npm]" |
Section 3: Project Structure
Purpose: Provide mental map of codebase organization
Token Budget: 200-300 tokens
Update Frequency: On major refactoring
## 3. PROJECT STRUCTURE
root/
├── apps/
│ ├── web/ # Next.js frontend (primary app)
│ │ ├── app/ # App Router pages
│ │ │ ├── (auth)/ # Auth routes group
│ │ │ ├── (dashboard)/ # Protected dashboard routes
│ │ │ └── api/ # REST API fallback (minimal use)
│ │ ├── components/
│ │ │ ├── ui/ # shadcn/ui primitives (auto-generated)
│ │ │ └── features/ # Business logic components
│ │ ├── lib/
│ │ │ ├── api/ # tRPC client utilities
│ │ │ ├── auth/ # NextAuth configuration
│ │ │ └── utils/ # Helper functions
│ │ └── styles/ # Global CSS
│ │
│ └── admin/ # Separate admin dashboard (future)
│
├── packages/
│ ├── ui/ # Shared component library
│ ├── database/ # Drizzle schema & migrations
│ │ ├── schema/ # Table definitions
│ │ ├── migrations/ # SQL migration files
│ │ └── seed/ # Test data generators
│ ├── api/ # tRPC routers (backend logic)
│ │ ├── routers/ # Feature-specific routers
│ │ ├── middleware/ # Auth, logging, etc.
│ │ └── context.ts # Request context builder
│ ├── types/ # Shared TypeScript types
│ └── config/ # ESLint, TypeScript, Tailwind configs
│
├── docs/ # Architecture decision records (ADRs)
├── scripts/ # Build and deployment scripts
└── .github/ # CI/CD workflows
### Key Directory Explanations
**`apps/web/app/`**: Next.js 15 App Router structure
- `(auth)/`: Route group for authentication pages (login, register)
- `(dashboard)/`: Protected routes requiring authentication
- Parallel routes use `@` prefix (e.g., `@modal`)
**`packages/database/`**: Single source of truth for schema
- NEVER modify schema in multiple places
- Always generate migrations: `pnpm db:generate`
- Apply to local dev: `pnpm db:push`
**`packages/api/`**: Backend business logic
- Each router represents a feature domain (users, products, orders)
- tRPC provides end-to-end type safety
- Procedures are functions callable from frontend
### File Naming Conventions
- **Components**: PascalCase (`UserProfile.tsx`)
- **Utilities**: camelCase (`formatCurrency.ts`)
- **Routes**: lowercase-with-dashes (`user-settings/`)
- **Types**: PascalCase (`User.ts`, `ApiResponse.ts`)
- **Tests**: Adjacent to source (`utils.test.ts`)
Advanced Pattern: ASCII Diagrams
### Data Flow Architecture
┌─────────────┐
│ Browser │
└──────┬──────┘
│ HTTP/WebSocket
▼
┌─────────────────┐
│ Next.js (SSR) │◄─────┐
└────┬───────┬────┘ │
│ │ │
│ └──────┐ │
▼ ▼ │
┌─────────┐ ┌─────────┴───┐
│ tRPC │ │ Server │
│ Client │ │ Components │
└────┬────┘ └─────────────┘
│ Type-safe RPC
▼
┌────────────────┐
│ tRPC Router │
│ (Backend) │
└────┬───────────┘
│
▼
┌────────────────┐
│ Drizzle ORM │
└────┬───────────┘
│ SQL
▼
┌────────────────┐
│ PostgreSQL │
└────────────────┘
Section 4: Coding Standards
Purpose: Define exact code patterns and anti-patterns
Token Budget: 600-800 tokens (largest section)
Update Frequency: Weekly incremental additions
## 4. CODING STANDARDS
### TypeScript Configuration
**tsconfig.json Settings (Enforced)**:
json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUncheckedIndexedAccess": true,
"forceConsistentCasingInFileNames": true
}
}
### Type Annotations
#### ✅ REQUIRED: Explicit Return Types
typescript
// Correct: Explicit return type
export function calculateTax(amount: number, rate: number): number {
return amount * rate;
}
// Correct: Explicit Promise return type
export async function fetchUser(id: string): Promise {
const user = await db.query.users.findFirst({
where: eq(users.id, id)
});
return user;
}
// Correct: Result type for operations that can fail
export async function createOrder(
data: OrderInput
): Promise> {
try {
const order = await db.insert(orders).values(data).returning();
return { success: true, data: order[0] };
} catch (error) {
logger.error('createOrder failed', { data, error });
return { success: false, error: 'Failed to create order' };
}
}
#### ❌ FORBIDDEN: Implicit Return Types
typescript
// Wrong: No return type specified
export function calculateTax(amount: number, rate: number) {
return amount * rate; // TypeScript infers 'number' but we require explicit
}
// Wrong: Implicit any return
export async function fetchUser(id: string) {
return await db.query.users.findFirst({ where: eq(users.id, id) });
}
### Error Handling Pattern (MANDATORY)
**Standard Result Type**:
typescript
// types/result.ts (use everywhere)
export type Result =
| { success: true; data: T }
| { success: false; error: string };
**Implementation Template**:
typescript
export async function databaseOperation(
input: InputType
): Promise> {
try {
// Step 1: Validate input (if not done by tRPC)
const validated = InputSchema.parse(input);
// Step 2: Perform database operation
const result = await db.transaction(async (tx) => {
const data = await tx.insert(table).values(validated).returning();
return data[0];
});
// Step 3: Check for business logic failures
if (!result) {
return { success: false, error: 'Operation failed' };
}
// Step 4: Return success
return { success: true, data: result };
} catch (error) {
// Step 5: Log with full context
logger.error('databaseOperation failed', {
input,
error: error instanceof Error ? error.message : 'Unknown',
stack: error instanceof Error ? error.stack : undefined
});
// Step 6: Return user-friendly error
if (error instanceof z.ZodError) {
return { success: false, error: 'Invalid input data' };
}
return { success: false, error: 'Internal error occurred' };
}
}
**Checklist for Every Async Function**:
- [ ] Returns `Promise<Result<T>>`
- [ ] Has try-catch block
- [ ] Logs errors with context (input, error message, stack trace)
- [ ] Returns user-friendly error messages (never leak internals)
- [ ] Validates inputs before processing
- [ ] Uses transactions for multi-step operations
### React Component Patterns
#### Server Components (Default)
tsx
// app/users/[id]/page.tsx
// NO 'use client' directive = Server Component
export default async function UserPage({
params
}: {
params: { id: string }
}) {
// Can fetch data directly (no useEffect needed)
const user = await db.query.users.findFirst({
where: eq(users.id, params.id)
});
if (!user) {
notFound(); // Next.js 15 helper
}
return (
{user.name}
);
}
**When to Use**:
- ✅ Fetching data from database
- ✅ Reading environment variables
- ✅ Server-side computations
- ✅ SEO-critical content
**Benefits**:
- Zero JavaScript sent to client
- Direct database access
- No loading states needed
- Automatic request deduplication
#### Client Components (Only When Required)
tsx
// components/features/InteractiveCart.tsx
'use client'; // Required directive at top of file
import { useState } from 'react';
import { trpc } from '@/lib/trpc/client';
export function InteractiveCart() {
// useState requires 'use client'
const [quantity, setQuantity] = useState(1);
// tRPC hooks require 'use client'
const { data: cart, mutate } = trpc.cart.get.useQuery();
const addItem = trpc.cart.addItem.useMutation();
// Event handlers require 'use client'
const handleAddToCart = async () => {
await addItem.mutateAsync({ productId: '...', quantity });
};
return (
type="number"
value={quantity}
onChange={(e) => setQuantity(Number(e.target.value))}
/>
Add to Cart
);
}
**Add 'use client' ONLY When**:
- ✅ Using hooks (useState, useEffect, useContext, etc.)
- ✅ Event handlers (onClick, onChange, onSubmit)
- ✅ Browser APIs (window, localStorage, navigator)
- ✅ Third-party libraries that use client features
- ✅ CSS-in-JS libraries (styled-components, emotion)
**Common Mistake**:
tsx
// ❌ WRONG: Unnecessary 'use client'
'use client';
export function StaticHeader() {
return
Welcome to Our Store
;// No hooks, no events = should be Server Component
}
// ✅ CORRECT: Remove 'use client' directive
export function StaticHeader() {
return
Welcome to Our Store
;}
### Import Organization (Enforced by ESLint)
typescript
// 1. React and external dependencies (alphabetical)
import { useState, useEffect } from 'react';
import { z } from 'zod';
import { TRPCError } from '@trpc/server';
// 2. Internal packages from monorepo (@workspace/*)
import { Button } from '@repo/ui';
import { User, Product } from '@repo/types';
// 3. Application modules using @ alias
import { trpc } from '@/lib/trpc/client';
import { cn, formatCurrency } from '@/lib/utils';
import { logger } from '@/lib/logger';
// 4. Relative imports (same directory or parent)
import { UserCard } from './UserCard';
import { useUser } from '../hooks/useUser';
import type { UserCardProps } from './types';
// 5. CSS/style imports last
import './UserProfile.css';
### Naming Conventions
| Element | Convention | Example | Counter-Example |
|---------|-----------|---------|-----------------|
| **Variables** | camelCase | `userName`, `isLoading` | `user_name`, `IsLoading` |
| **Functions** | camelCase | `fetchUserData()`, `calculateTotal()` | `FetchUserData()`, `fetch_user_data()` |
| **React Components** | PascalCase | `UserProfile`, `ProductCard` | `userProfile`, `product-card` |
| **Types/Interfaces** | PascalCase | `User`, `ApiResponse<T>` | `user`, `api_response` |
| **Constants** | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT`, `API_BASE_URL` | `maxRetryCount`, `apiBaseUrl` |
| **Files (components)** | PascalCase | `UserProfile.tsx` | `user-profile.tsx`, `userProfile.tsx` |
| **Files (utilities)** | camelCase | `formatDate.ts`, `apiClient.ts` | `FormatDate.ts`, `api_client.ts` |
| **Directories** | kebab-case | `user-settings/`, `api-routes/` | `userSettings/`, `api_routes/` |
| **Private functions** | prefix `_` | `_validateInput()`, `_processData()` | (no prefix) |
### Zod Validation Pattern
typescript
// schemas/user.ts
import { z } from 'zod';
// Define schema once
export const CreateUserSchema = z.object({
email: z.string().email('Invalid email format'),
name: z.string().min(2, 'Name must be at least 2 characters'),
age: z.number().int().min(18, 'Must be 18 or older').optional(),
role: z.enum(['user', 'admin', 'moderator']).default('user')
});
// Infer TypeScript type from schema
export type CreateUserInput = z.infer;
// Usage in tRPC procedure
export const userRouter = createTRPCRouter({
create: protectedProcedure
.input(CreateUserSchema) // Automatic validation
.mutation(async ({ input, ctx }) => {
// input is typed as CreateUserInput and already validated
const user = await ctx.db.insert(users).values(input).returning();
return user[0];
})
});
**Schema Best Practices**:
- ✅ One schema file per domain entity
- ✅ Reuse schemas across API and client
- ✅ Provide helpful error messages
- ✅ Use `.describe()` for documentation
- ❌ Don't duplicate schemas (DRY principle)
Section 5: Common Commands
Purpose: Prevent Claude from asking "how do I...?" repeatedly
Token Budget: 150-200 tokens
Update Frequency: When scripts change
## 5. COMMON COMMANDS
### Development Workflow
bash
Start development server
pnpm dev
Starts: Next.js (localhost:3000), watches for file changes
Start specific app in monorepo
pnpm dev --filter=web
pnpm dev --filter=admin
Type checking (runs TSC)
pnpm type-check
Must pass with 0 errors before committing
Linting
pnpm lint
Runs ESLint on all packages
Linting with auto-fix
pnpm lint --fix
Fixes auto-fixable issues
Format code
pnpm format
Runs Prettier on all files
Format check (CI)
pnpm format:check
Exits 1 if files not formatted
### Testing Commands
bash
Run all unit tests
pnpm test
Vitest runner, watch mode disabled
Run tests in watch mode (development)
pnpm test:watch
Re-runs tests on file changes
Run tests with coverage report
pnpm test:coverage
Generates HTML report in coverage/
Run integration tests
pnpm test:integration
Requires database connection
Run E2E tests
pnpm test:e2e
Launches Playwright, runs against local server
Run E2E in UI mode (debugging)
pnpm test:e2e:ui
Opens Playwright UI for step-through debugging
### Database Commands
bash
Push schema changes to dev database (no migrations)
pnpm db:push
WARNING: Destructive in production, use for dev only
Generate migration from schema changes
pnpm db:generate
Creates SQL file in packages/database/migrations/
Apply migrations to database
pnpm db:migrate
Runs pending migrations
Open Drizzle Studio (database GUI)
pnpm db:studio
Opens browser at localhost:4983
Seed database with test data
pnpm db:seed
Runs seed scripts from packages/database/seed/
Reset database (drop all + recreate)
pnpm db:reset
Destructive: drops all tables and re-runs migrations
### Build & Deployment
bash
Production build
pnpm build
Creates optimized bundles in dist/ or .next/
Build specific package
pnpm build --filter=web
Clean build artifacts
pnpm clean
Removes node_modules, dist, .next, .turbo
Deploy to staging (Railway)
pnpm deploy:staging
Triggers Railway deployment via CLI
Deploy to production (Vercel)
pnpm deploy:production
Triggers Vercel deployment via CLI
### Troubleshooting Commands
bash
Clear Turborepo cache
rm -rf .turbo
Clear Next.js cache
rm -rf apps/web/.next
Reinstall all dependencies
pnpm clean && pnpm install
Check for dependency issues
pnpm check
Update dependencies (interactive)
pnpm update --interactive
### When Claude Should Execute Commands
**Automatically execute** (no confirmation needed):
- `pnpm type-check` (validation)
- `pnpm lint` (checking)
- `pnpm test` (verification)
**Ask before executing** (potentially destructive):
- `pnpm db:push` (modifies database)
- `pnpm db:reset` (drops data)
- `pnpm deploy:*` (affects live systems)
- `rm -rf` (deletes files)
Section 6: Testing Requirements
Purpose: Define comprehensive testing strategy
Token Budget: 300-400 tokens
Update Frequency: On testing approach changes
## 6. TESTING REQUIREMENTS
### Coverage Targets (Enforced by CI)
| Test Type | Minimum Coverage | Measured By | Failure Action |
|-----------|-----------------|-------------|----------------|
| **Unit Tests** | 80% | Lines + Branches | Block PR merge |
| **Integration Tests** | 70% | API endpoints | Block PR merge |
| **E2E Tests** | Critical paths only | User flows | Warning only |
### Test Structure: AAA Pattern (Mandatory)
typescript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
describe('UserService', () => {
// Setup runs before each test
beforeEach(() => {
vi.clearAllMocks();
});
// Cleanup runs after each test
afterEach(() => {
vi.restoreAllMocks();
});
describe('createUser', () => {
it('should create user with valid data', async () => {
// ARRANGE: Set up test data and mocks
const mockDb = createMockDatabase();
const service = new UserService(mockDb);
const userData = {
email: 'test@example.com',
name: 'Test User'
};
// ACT: Execute the function being tested
const result = await service.createUser(userData);
// ASSERT: Verify the outcome
expect(result.success).toBe(true);
expect(result.data).toMatchObject({
email: userData.email,
name: userData.name
});
expect(mockDb.insert).toHaveBeenCalledOnce();
});
it('should return error for duplicate email', async () => {
// ARRANGE
const mockDb = createMockDatabase();
mockDb.insert.mockRejectedValueOnce(
new Error('UNIQUE constraint failed')
);
const service = new UserService(mockDb);
// ACT
const result = await service.createUser({
email: 'existing@example.com',
name: 'Test'
});
// ASSERT
expect(result.success).toBe(false);
expect(result.error).toContain('email already exists');
});
it('should handle edge case: empty name after trim', async () => {
// ARRANGE
const service = new UserService(createMockDatabase());
// ACT
const result = await service.createUser({
email: 'test@example.com',
name: ' ' // Only whitespace
});
// ASSERT
expect(result.success).toBe(false);
expect(result.error).toContain('Name is required');
});
});
});
### Test Naming Convention
typescript
// Pattern: "should [expected behavior] when [condition]"
it('should return user when valid ID provided', async () => {});
it('should throw error when user not found', async () => {});
it('should validate email format before saving', async () => {});
it('should handle concurrent requests without race conditions', async () => {});
### Mocking Strategies
#### Mock External APIs (MSW)
typescript
import { rest } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
rest.get('https://api.stripe.com/v1/customers', (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
id: 'cus_test123',
email: 'customer@example.com'
})
);
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
#### Mock Database (Drizzle)
typescript
const mockDb = {
query: {
users: {
findFirst: vi.fn(),
findMany: vi.fn()
}
},
insert: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
transaction: vi.fn((callback) => callback(mockDb))
};
#### Mock React Hooks
typescript
vi.mock('@/hooks/useAuth', () => ({
useAuth: () => ({
user: { id: '1', name: 'Test User', email: 'test@example.com' },
isAuthenticated: true,
isLoading: false
})
}));
### Component Testing (React Testing Library)
typescript
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserProfile } from './UserProfile';
it('should display user information after loading', async () => {
// ARRANGE
const mockUser = { id: '1', name: 'John Doe', email: 'john@example.com' };
render();
// ACT - Wait for async data load
await waitFor(() => {
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});
// ASSERT
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('john@example.com')).toBeInTheDocument();
});
it('should submit form when button clicked', async () => {
// ARRANGE
const user = userEvent.setup();
const onSubmit = vi.fn();
render();
// ACT
await user.type(screen.getByLabelText('Name'), 'Jane Doe');
await user.type(screen.getByLabelText('Email'), 'jane@example.com');
await user.click(screen.getByRole('button', { name: 'Submit' }));
// ASSERT
expect(onSubmit).toHaveBeenCalledWith({
name: 'Jane Doe',
email: 'jane@example.com'
});
});
### E2E Testing (Playwright)
typescript
import { test, expect } from '@playwright/test';
test.describe('User Registration Flow', () => {
test('should complete full registration and login', async ({ page }) => {
// Navigate to registration page
await page.goto('/register');
// Fill registration form
await page.fill('[name="email"]', 'newuser@example.com');
await page.fill('[name="password"]', 'SecurePass123!');
await page.fill('[name="confirmPassword"]', 'SecurePass123!');
await page.fill('[name="name"]', 'New User');
// Submit form
await page.click('button[type="submit"]');
// Verify redirect to dashboard
await expect(page).toHaveURL('/dashboard');
// Verify welcome message
await expect(page.locator('text=Welcome, New User')).toBeVisible();
// Log out
await page.click('[data-testid="user-menu"]');
await page.click('text=Logout');
// Verify redirect to home
await expect(page).toHaveURL('/');
// Log back in
await page.goto('/login');
await page.fill('[name="email"]', 'newuser@example.com');
await page.fill('[name="password"]', 'SecurePass123!');
await page.click('button[type="submit"]');
// Verify successful login
await expect(page).toHaveURL('/dashboard');
});
test('should display validation errors for invalid input', async ({ page }) => {
await page.goto('/register');
// Submit without filling form
await page.click('button[type="submit"]');
// Check for validation messages
await expect(page.locator('text=Email is required')).toBeVisible();
await expect(page.locator('text=Password is required')).toBeVisible();
// Fill invalid email
await page.fill('[name="email"]', 'invalid-email');
await page.blur('[name="email"]');
await expect(page.locator('text=Invalid email format')).toBeVisible();
});
});
### Test File Organization
src/
├── services/
│ ├── UserService.ts
│ └── UserService.test.ts # Unit test adjacent to source
├── components/
│ ├── UserProfile.tsx
│ └── UserProfile.test.tsx # Component test adjacent
└── tests/
├── integration/
│ └── api/
│ └── users.test.ts # Integration tests separate
└── e2e/
└── registration.spec.ts # E2E tests separate
### When to Write Each Test Type
| Test Type | Write When | Example Scenario |
|-----------|-----------|------------------|
| **Unit** | Creating pure functions, services, utilities | `calculateTax()`, `UserService.create()` |
| **Component** | Building UI components with logic | Form validation, conditional rendering |
| **Integration** | Creating API endpoints | tRPC procedures, REST routes |
| **E2E** | Implementing critical user flows | Registration, checkout, account deletion |
Section 7: Known Issues & Pitfalls
Purpose: Prevent Claude from repeating known mistakes
Token Budget: 300-400 tokens
Update Frequency: As issues discovered
## 7. KNOWN ISSUES & PITFALLS
### Issue #1: Hot Module Replacement with tRPC
**Symptom**: Changes to tRPC routers don't reflect in frontend; type errors about missing procedures.
**Root Cause**: tRPC code generation lags behind file system changes; Next.js HMR doesn't trigger full rebuild.
**Solution**:
bash
Restart development server
Ctrl+C
pnpm dev
**Prevention**: Configure Next.js to watch tRPC files:
javascript
// next.config.js
module.exports = {
webpack: (config) => {
config.watchOptions = {
...config.watchOptions,
ignored: ['/node_modules', '!/packages/api/**'],
};
return config;
}
};
**Tracking**: GitHub Issue #123
---
### Issue #2: Server Component Hydration Errors
**Symptom**: Console error: "Text content did not match. Server: X Client: Y"
**Root Cause**: Async Server Components not awaited, or mixing server/client data incorrectly.
**Example of Bug**:
tsx
// ❌ WRONG: Not awaiting async call
export default function UserPage() {
const user = fetchUser(); // Missing await!
return
}
**Fix**:
tsx
// ✅ CORRECT: Async component with await
export default async function UserPage() {
const user = await fetchUser(); // Properly awaited
return
}
**Additional Cause**: Using browser APIs in Server Components
tsx
// ❌ WRONG: localStorage in Server Component
export default function Page() {
const value = localStorage.getItem('key'); // Browser API!
return
}
**Fix**: Move to Client Component
tsx
'use client';
export default function Page() {
const value = localStorage.getItem('key'); // Now OK
return
}
---
### Issue #3: Drizzle Query Performance (N+1 Problem)
**Symptom**: Slow dashboard loads (>2 seconds); database shows hundreds of queries.
**Root Cause**: N+1 query anti-pattern—fetching related data in loops.
**Example of Bug**:
typescript
// ❌ WRONG: N+1 queries (1 for users + N for posts)
const users = await db.query.users.findMany();
for (const user of users) {
user.posts = await db.query.posts.findMany({
where: eq(posts.userId, user.id)
});
}
**Fix**: Use eager loading with `with`
typescript
// ✅ CORRECT: Single query with joins
const users = await db.query.users.findMany({
with: {
posts: {
limit: 10,
orderBy: desc(posts.createdAt)
},
profile: true
}
});
**Performance Impact**: Reduced from 127ms to 8ms in production.
---
### Issue #4: Environment Variables Not Available on Client
**Symptom**: `process.env.NEXT_PUBLIC_API_URL` is undefined in browser; works on server.
**Root Cause**: Next.js only exposes variables prefixed with `NEXT_PUBLIC_` to client-side code.
**Solution**:
bash
.env.local
✅ Available on client (has prefix)
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_STRIPE_KEY=pk_test_xxx
❌ Server-only (no prefix)
DATABASE_URL=postgresql://...
STRIPE_SECRET=sk_test_xxx
**Validation**: Use `@t3-oss/env-nextjs` to validate at build time
typescript
// env.ts
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const env = createEnv({
server: {
DATABASE_URL: z.string().url(),
STRIPE_SECRET: z.string().min(20),
},
client: {
NEXT_PUBLIC_API_URL: z.string().url(),
NEXT_PUBLIC_STRIPE_KEY: z.string().min(20),
},
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
STRIPE_SECRET: process.env.STRIPE_SECRET,
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
NEXT_PUBLIC_STRIPE_KEY: process.env.NEXT_PUBLIC_STRIPE_KEY,
},
});
---
### Issue #5: pnpm Workspace Protocol Errors
**Symptom**: Import fails with "Cannot find module '@repo/ui'"
**Root Cause**: pnpm workspace dependencies use `workspace:*` protocol; not resolved properly.
**Check**:
json
// packages/web/package.json
{
"dependencies": {
"@repo/ui": "workspace:*" // Should be present
}
}
**Fix**:
bash
Reinstall to rebuild workspace links
pnpm install
Verify workspace structure
pnpm list --depth 0
---
### Issue #6: TypeScript Path Aliases Not Resolving
**Symptom**: Import using `@/lib/utils` fails with "Cannot find module"
**Root Cause**: `tsconfig.json` paths not configured or not extended properly.
**Solution**: Verify all `tsconfig.json` files extend base config
json
// apps/web/tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/": ["./src/"],
"@/components/": ["./src/components/"]
}
}
}
**Also check**: `next.config.js` experimental settings
javascript
module.exports = {
experimental: {
typedRoutes: true, // Can affect path resolution
}
};
Section 8: Important Notes
Purpose: Critical project-specific constraints
Token Budget: 200-300 tokens
Update Frequency: On business logic changes
## 8. IMPORTANT NOTES
### API & Rate Limiting
**Rate Limits** (enforced by Upstash Redis):
- **Public endpoints**: 100 requests/minute per IP
- **Authenticated users**: 1,000 requests/minute per user ID
- **Admin endpoints**: 10,000 requests/minute (no limit practically)
**Implementation Location**: `packages/api/middleware/rateLimit.ts`
**Rate Limit Headers** (returned in responses):
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1732900800
**API Versioning**:
- All routes prefixed with `/api/v1/`
- Legacy v0 APIs deprecated (return 410 Gone)
- Current version: v1 (stable since 2025-08-01)
---
### Authentication & Security
**JWT Configuration**:
- **Access Token Expiration**: 15 minutes
- **Refresh Token Expiration**: 7 days
- **Refresh Token Rotation**: Enabled (new token issued on refresh)
**
Top comments (0)