DEV Community

Yigit Konur
Yigit Konur

Posted on • Edited on

The Ultimate Guide to CLAUDE.md: Best Practices to Turn Claude Into Super-Powered AI Teammate

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
Enter fullscreen mode Exit fullscreen mode

Loading Behavior & Merging

How Claude Reads Files:

  1. Starts at current working directory
  2. Recurses UP the file tree (child → parent → root)
  3. Combines all found files (more specific overrides more general)
  4. 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
Enter fullscreen mode Exit fullscreen mode

Active context when editing App.tsx:

  1. Personal: ~/.claude/CLAUDE.md
  2. Monorepo: /home/user/projects/monorepo/CLAUDE.md
  3. 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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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`)
Enter fullscreen mode Exit fullscreen mode

Advanced Pattern: ASCII Diagrams

### Data Flow Architecture

Enter fullscreen mode Exit fullscreen mode

┌─────────────┐
│ Browser │
└──────┬──────┘
│ HTTP/WebSocket

┌─────────────────┐
│ Next.js (SSR) │◄─────┐
└────┬───────┬────┘ │
│ │ │
│ └──────┐ │
▼ ▼ │
┌─────────┐ ┌─────────┴───┐
│ tRPC │ │ Server │
│ Client │ │ Components │
└────┬────┘ └─────────────┘
│ Type-safe RPC

┌────────────────┐
│ tRPC Router │
│ (Backend) │
└────┬───────────┘


┌────────────────┐
│ Drizzle ORM │
└────┬───────────┘
│ SQL

┌────────────────┐
│ PostgreSQL │
└────────────────┘

Enter fullscreen mode Exit fullscreen mode

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)**:
Enter fullscreen mode Exit fullscreen mode


json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUncheckedIndexedAccess": true,
"forceConsistentCasingInFileNames": true
}
}


### Type Annotations

#### ✅ REQUIRED: Explicit Return Types

Enter fullscreen mode Exit fullscreen mode


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

Enter fullscreen mode Exit fullscreen mode


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**:

Enter fullscreen mode Exit fullscreen mode


typescript
// types/result.ts (use everywhere)
export type Result =
| { success: true; data: T }
| { success: false; error: string };


**Implementation Template**:

Enter fullscreen mode Exit fullscreen mode


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 };
Enter fullscreen mode Exit fullscreen mode

} 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' };
Enter fullscreen mode Exit fullscreen mode

}
}


**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)

Enter fullscreen mode Exit fullscreen mode


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)

Enter fullscreen mode Exit fullscreen mode


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**:

Enter fullscreen mode Exit fullscreen mode


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)

Enter fullscreen mode Exit fullscreen mode


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

Enter fullscreen mode Exit fullscreen mode


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)
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode


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

Enter fullscreen mode Exit fullscreen mode


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

Enter fullscreen mode Exit fullscreen mode


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

Enter fullscreen mode Exit fullscreen mode


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

Enter fullscreen mode Exit fullscreen mode


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)
Enter fullscreen mode Exit fullscreen mode

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)

Enter fullscreen mode Exit fullscreen mode


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');
});
Enter fullscreen mode Exit fullscreen mode

});
});


### Test Naming Convention

Enter fullscreen mode Exit fullscreen mode


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)

Enter fullscreen mode Exit fullscreen mode


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)

Enter fullscreen mode Exit fullscreen mode


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

Enter fullscreen mode Exit fullscreen mode


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)

Enter fullscreen mode Exit fullscreen mode


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)

Enter fullscreen mode Exit fullscreen mode


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');
Enter fullscreen mode Exit fullscreen mode

});

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();
Enter fullscreen mode Exit fullscreen mode

});
});


### Test File Organization

Enter fullscreen mode Exit fullscreen mode

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 |
Enter fullscreen mode Exit fullscreen mode

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**:
Enter fullscreen mode Exit fullscreen mode


bash

Restart development server

Ctrl+C
pnpm dev


**Prevention**: Configure Next.js to watch tRPC files:
Enter fullscreen mode Exit fullscreen mode


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**:
Enter fullscreen mode Exit fullscreen mode


tsx
// ❌ WRONG: Not awaiting async call
export default function UserPage() {
const user = fetchUser(); // Missing await!
return

{user.name};
}

**Fix**:
Enter fullscreen mode Exit fullscreen mode


tsx
// ✅ CORRECT: Async component with await
export default async function UserPage() {
const user = await fetchUser(); // Properly awaited
return

{user.name};
}

**Additional Cause**: Using browser APIs in Server Components
Enter fullscreen mode Exit fullscreen mode


tsx
// ❌ WRONG: localStorage in Server Component
export default function Page() {
const value = localStorage.getItem('key'); // Browser API!
return

{value};
}

**Fix**: Move to Client Component
Enter fullscreen mode Exit fullscreen mode


tsx
'use client';

export default function Page() {
const value = localStorage.getItem('key'); // Now OK
return

{value};
}

---

### 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**:
Enter fullscreen mode Exit fullscreen mode


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`
Enter fullscreen mode Exit fullscreen mode


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**:
Enter fullscreen mode Exit fullscreen mode


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
Enter fullscreen mode Exit fullscreen mode


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**:
Enter fullscreen mode Exit fullscreen mode


json
// packages/web/package.json
{
"dependencies": {
"@repo/ui": "workspace:*" // Should be present
}
}


**Fix**:
Enter fullscreen mode Exit fullscreen mode


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
Enter fullscreen mode Exit fullscreen mode


json
// apps/web/tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/": ["./src/"],
"@/components/": ["./src/components/"]
}
}
}


**Also check**: `next.config.js` experimental settings
Enter fullscreen mode Exit fullscreen mode


javascript
module.exports = {
experimental: {
typedRoutes: true, // Can affect path resolution
}
};

Enter fullscreen mode Exit fullscreen mode

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):
Enter fullscreen mode Exit fullscreen mode

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)

**

Enter fullscreen mode Exit fullscreen mode

Top comments (0)