Add new skills for backend patterns, coding standards, continuous learning, and NestJS best practices

- Introduced backend patterns skill with guidelines on API design, database optimization, and server-side best practices.
- Added coding standards skill outlining universal coding principles for TypeScript, NestJS, and Node.js development.
- Implemented continuous learning skill to automatically extract reusable patterns from Cursor sessions.
- Created NestJS best practices skill detailing architecture patterns, dependency injection, error handling, and security measures.
- Included various rules and templates for NestJS best practices to ensure production-ready applications.
This commit is contained in:
shancheas
2026-08-20 18:30:39 +07:00
commit 0b0bdd9c4b
108 changed files with 16056 additions and 0 deletions
+564
View File
@@ -0,0 +1,564 @@
---
name: backend-patterns
description: Backend architecture patterns, API design, database optimization, and server-side best practices for NestJS and Drizzle.
---
# Backend Development Patterns
Backend architecture patterns and best practices for scalable NestJS applications.
## API Design Patterns
### RESTful API Structure
```typescript
// ✅ Resource-based URLs
GET /api/markets # List resources
GET /api/markets/:id # Get single resource
POST /api/markets # Create resource
PUT /api/markets/:id # Replace resource
PATCH /api/markets/:id # Update resource
DELETE /api/markets/:id # Delete resource
// ✅ Query parameters for filtering, sorting, pagination
GET /api/markets?status=active&sort=volume&limit=20&offset=0
```
### Repository Pattern
```typescript
// Abstract data access logic
interface MarketRepository {
findAll(filters?: MarketFilters): Promise<Market[]>
findById(id: string): Promise<Market | null>
create(data: CreateMarketDto): Promise<Market>
update(id: string, data: UpdateMarketDto): Promise<Market>
delete(id: string): Promise<void>
}
class DrizzleMarketRepository implements MarketRepository {
constructor(private readonly db: DrizzleDB) {}
async findAll(filters?: MarketFilters): Promise<Market[]> {
let query = this.db.select().from(markets)
if (filters?.status) {
query = query.where(eq(markets.status, filters.status)) as typeof query
}
if (filters?.limit) {
query = query.limit(filters.limit) as typeof query
}
return query
}
// Other methods...
}
```
### Service Layer Pattern
```typescript
// Business logic separated from data access
class MarketService {
constructor(private marketRepo: MarketRepository) {}
async searchMarkets(query: string, limit: number = 10): Promise<Market[]> {
// Business logic
const embedding = await generateEmbedding(query)
const results = await this.vectorSearch(embedding, limit)
// Fetch full data
const markets = await this.marketRepo.findByIds(results.map(r => r.id))
// Sort by similarity
return markets.sort((a, b) => {
const scoreA = results.find(r => r.id === a.id)?.score || 0
const scoreB = results.find(r => r.id === b.id)?.score || 0
return scoreA - scoreB
})
}
private async vectorSearch(embedding: number[], limit: number) {
// Vector search implementation
}
}
```
### Middleware Pattern
```typescript
// Request/response processing pipeline
export function withAuth(handler: NextApiHandler): NextApiHandler {
return async (req, res) => {
const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) {
return res.status(401).json({ error: 'Unauthorized' })
}
try {
const user = await verifyToken(token)
req.user = user
return handler(req, res)
} catch (error) {
return res.status(401).json({ error: 'Invalid token' })
}
}
}
// Usage
export default withAuth(async (req, res) => {
// Handler has access to req.user
})
```
## Database Patterns
### Query Optimization
```typescript
// ✅ GOOD: Select only needed columns
const data = await db
.select({
id: markets.id,
name: markets.name,
status: markets.status,
volume: markets.volume,
})
.from(markets)
.where(eq(markets.status, 'active'))
.orderBy(desc(markets.volume))
.limit(10)
// ❌ BAD: Select everything
const data = await db.select().from(markets)
```
### N+1 Query Prevention
```typescript
// ❌ BAD: N+1 query problem
const markets = await getMarkets()
for (const market of markets) {
market.creator = await getUser(market.creator_id) // N queries
}
// ✅ GOOD: Batch fetch
const markets = await getMarkets()
const creatorIds = markets.map(m => m.creator_id)
const creators = await getUsers(creatorIds) // 1 query
const creatorMap = new Map(creators.map(c => [c.id, c]))
markets.forEach(market => {
market.creator = creatorMap.get(market.creator_id)
})
```
### Transaction Pattern
```typescript
async function createMarketWithPosition(
marketData: CreateMarketDto,
positionData: CreatePositionDto
) {
return db.transaction(async (tx) => {
const [market] = await tx.insert(markets).values(marketData).returning()
const [position] = await tx
.insert(positions)
.values({ ...positionData, marketId: market.id })
.returning()
return { market, position }
})
}
```
## Caching Strategies
### Redis Caching Layer
```typescript
class CachedMarketRepository implements MarketRepository {
constructor(
private baseRepo: MarketRepository,
private redis: RedisClient
) {}
async findById(id: string): Promise<Market | null> {
// Check cache first
const cached = await this.redis.get(`market:${id}`)
if (cached) {
return JSON.parse(cached)
}
// Cache miss - fetch from database
const market = await this.baseRepo.findById(id)
if (market) {
// Cache for 5 minutes
await this.redis.setex(`market:${id}`, 300, JSON.stringify(market))
}
return market
}
async invalidateCache(id: string): Promise<void> {
await this.redis.del(`market:${id}`)
}
}
```
### Cache-Aside Pattern
```typescript
async function getMarketWithCache(id: string): Promise<Market> {
const cacheKey = `market:${id}`
// Try cache
const cached = await redis.get(cacheKey)
if (cached) return JSON.parse(cached)
// Cache miss - fetch from DB
const market = await db.markets.findUnique({ where: { id } })
if (!market) throw new Error('Market not found')
// Update cache
await redis.setex(cacheKey, 300, JSON.stringify(market))
return market
}
```
## Error Handling Patterns
### Centralized Error Handler
```typescript
class ApiError extends Error {
constructor(
public statusCode: number,
public message: string,
public isOperational = true
) {
super(message)
Object.setPrototypeOf(this, ApiError.prototype)
}
}
export function errorHandler(error: unknown, req: Request): Response {
if (error instanceof ApiError) {
return NextResponse.json({
success: false,
error: error.message
}, { status: error.statusCode })
}
if (error instanceof z.ZodError) {
return NextResponse.json({
success: false,
error: 'Validation failed',
details: error.errors
}, { status: 400 })
}
// Log unexpected errors
console.error('Unexpected error:', error)
return NextResponse.json({
success: false,
error: 'Internal server error'
}, { status: 500 })
}
// Usage
export async function GET(request: Request) {
try {
const data = await fetchData()
return NextResponse.json({ success: true, data })
} catch (error) {
return errorHandler(error, request)
}
}
```
### Retry with Exponential Backoff
```typescript
async function fetchWithRetry<T>(
fn: () => Promise<T>,
maxRetries = 3
): Promise<T> {
let lastError: Error
for (let i = 0; i < maxRetries; i++) {
try {
return await fn()
} catch (error) {
lastError = error as Error
if (i < maxRetries - 1) {
// Exponential backoff: 1s, 2s, 4s
const delay = Math.pow(2, i) * 1000
await new Promise(resolve => setTimeout(resolve, delay))
}
}
}
throw lastError!
}
// Usage
const data = await fetchWithRetry(() => fetchFromAPI())
```
## Authentication & Authorization
### JWT Token Validation
```typescript
import jwt from 'jsonwebtoken'
interface JWTPayload {
userId: string
email: string
role: 'admin' | 'user'
}
export function verifyToken(token: string): JWTPayload {
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload
return payload
} catch (error) {
throw new ApiError(401, 'Invalid token')
}
}
export async function requireAuth(request: Request) {
const token = request.headers.get('authorization')?.replace('Bearer ', '')
if (!token) {
throw new ApiError(401, 'Missing authorization token')
}
return verifyToken(token)
}
// Usage in API route
export async function GET(request: Request) {
const user = await requireAuth(request)
const data = await getDataForUser(user.userId)
return NextResponse.json({ success: true, data })
}
```
### Role-Based Access Control
```typescript
type Permission = 'read' | 'write' | 'delete' | 'admin'
interface User {
id: string
role: 'admin' | 'moderator' | 'user'
}
const rolePermissions: Record<User['role'], Permission[]> = {
admin: ['read', 'write', 'delete', 'admin'],
moderator: ['read', 'write', 'delete'],
user: ['read', 'write']
}
export function hasPermission(user: User, permission: Permission): boolean {
return rolePermissions[user.role].includes(permission)
}
export function requirePermission(permission: Permission) {
return async (request: Request) => {
const user = await requireAuth(request)
if (!hasPermission(user, permission)) {
throw new ApiError(403, 'Insufficient permissions')
}
return user
}
}
// Usage
export const DELETE = requirePermission('delete')(async (request: Request) => {
// Handler with permission check
})
```
## Rate Limiting
### Simple In-Memory Rate Limiter
```typescript
class RateLimiter {
private requests = new Map<string, number[]>()
async checkLimit(
identifier: string,
maxRequests: number,
windowMs: number
): Promise<boolean> {
const now = Date.now()
const requests = this.requests.get(identifier) || []
// Remove old requests outside window
const recentRequests = requests.filter(time => now - time < windowMs)
if (recentRequests.length >= maxRequests) {
return false // Rate limit exceeded
}
// Add current request
recentRequests.push(now)
this.requests.set(identifier, recentRequests)
return true
}
}
const limiter = new RateLimiter()
export async function GET(request: Request) {
const ip = request.headers.get('x-forwarded-for') || 'unknown'
const allowed = await limiter.checkLimit(ip, 100, 60000) // 100 req/min
if (!allowed) {
return NextResponse.json({
error: 'Rate limit exceeded'
}, { status: 429 })
}
// Continue with request
}
```
## Background Jobs & Queues
### Simple Queue Pattern
```typescript
class JobQueue<T> {
private queue: T[] = []
private processing = false
async add(job: T): Promise<void> {
this.queue.push(job)
if (!this.processing) {
this.process()
}
}
private async process(): Promise<void> {
this.processing = true
while (this.queue.length > 0) {
const job = this.queue.shift()!
try {
await this.execute(job)
} catch (error) {
console.error('Job failed:', error)
}
}
this.processing = false
}
private async execute(job: T): Promise<void> {
// Job execution logic
}
}
// Usage for indexing markets
interface IndexJob {
marketId: string
}
const indexQueue = new JobQueue<IndexJob>()
export async function POST(request: Request) {
const { marketId } = await request.json()
// Add to queue instead of blocking
await indexQueue.add({ marketId })
return NextResponse.json({ success: true, message: 'Job queued' })
}
```
## Logging & Monitoring
### Structured Logging
```typescript
interface LogContext {
userId?: string
requestId?: string
method?: string
path?: string
[key: string]: unknown
}
class Logger {
log(level: 'info' | 'warn' | 'error', message: string, context?: LogContext) {
const entry = {
timestamp: new Date().toISOString(),
level,
message,
...context
}
console.log(JSON.stringify(entry))
}
info(message: string, context?: LogContext) {
this.log('info', message, context)
}
warn(message: string, context?: LogContext) {
this.log('warn', message, context)
}
error(message: string, error: Error, context?: LogContext) {
this.log('error', message, {
...context,
error: error.message,
stack: error.stack
})
}
}
const logger = new Logger()
// Usage
export async function GET(request: Request) {
const requestId = crypto.randomUUID()
logger.info('Fetching markets', {
requestId,
method: 'GET',
path: '/api/markets'
})
try {
const markets = await fetchMarkets()
return NextResponse.json({ success: true, data: markets })
} catch (error) {
logger.error('Failed to fetch markets', error as Error, { requestId })
return NextResponse.json({ error: 'Internal error' }, { status: 500 })
}
}
```
**Remember**: Backend patterns enable scalable, maintainable server-side applications. Choose patterns that fit your complexity level.
+503
View File
@@ -0,0 +1,503 @@
---
name: coding-standards
description: Universal coding standards, best practices, and patterns for TypeScript, NestJS, and Node.js development.
---
# Coding Standards & Best Practices
Universal coding standards applicable across all projects.
## Code Quality Principles
### 1. Readability First
- Code is read more than written
- Clear variable and function names
- Self-documenting code preferred over comments
- Consistent formatting
### 2. KISS (Keep It Simple, Stupid)
- Simplest solution that works
- Avoid over-engineering
- No premature optimization
- Easy to understand > clever code
### 3. DRY (Don't Repeat Yourself)
- Extract common logic into functions
- Create reusable components
- Share utilities across modules
- Avoid copy-paste programming
### 4. YAGNI (You Aren't Gonna Need It)
- Don't build features before they're needed
- Avoid speculative generality
- Add complexity only when required
- Start simple, refactor when needed
## TypeScript/JavaScript Standards
### Variable Naming
```typescript
// ✅ GOOD: Descriptive names
const marketSearchQuery = 'election'
const isUserAuthenticated = true
const totalRevenue = 1000
// ❌ BAD: Unclear names
const q = 'election'
const flag = true
const x = 1000
```
### Function Naming
```typescript
// ✅ GOOD: Verb-noun pattern
async function fetchMarketData(marketId: string) { }
function calculateSimilarity(a: number[], b: number[]) { }
function isValidEmail(email: string): boolean { }
// ❌ BAD: Unclear or noun-only
async function market(id: string) { }
function similarity(a, b) { }
function email(e) { }
```
### Immutability Pattern (CRITICAL)
```typescript
// ✅ ALWAYS use spread operator
const updatedUser = {
...user,
name: 'New Name'
}
const updatedArray = [...items, newItem]
// ❌ NEVER mutate directly
user.name = 'New Name' // BAD
items.push(newItem) // BAD
```
### Error Handling
```typescript
// ✅ GOOD: Comprehensive error handling
async function fetchData(url: string) {
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return await response.json()
} catch (error) {
console.error('Fetch failed:', error)
throw new Error('Failed to fetch data')
}
}
// ❌ BAD: No error handling
async function fetchData(url) {
const response = await fetch(url)
return response.json()
}
```
### Async/Await Best Practices
```typescript
// ✅ GOOD: Parallel execution when possible
const [users, markets, stats] = await Promise.all([
fetchUsers(),
fetchMarkets(),
fetchStats()
])
// ❌ BAD: Sequential when unnecessary
const users = await fetchUsers()
const markets = await fetchMarkets()
const stats = await fetchStats()
```
### Type Safety
```typescript
// ✅ GOOD: Proper types
interface Market {
id: string
name: string
status: 'active' | 'resolved' | 'closed'
created_at: Date
}
function getMarket(id: string): Promise<Market> {
// Implementation
}
// ❌ BAD: Using 'any'
function getMarket(id: any): Promise<any> {
// Implementation
}
```
## React Best Practices
### Component Structure
```typescript
// ✅ GOOD: Functional component with types
interface ButtonProps {
children: React.ReactNode
onClick: () => void
disabled?: boolean
variant?: 'primary' | 'secondary'
}
export function Button({
children,
onClick,
disabled = false,
variant = 'primary'
}: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
className={`btn btn-${variant}`}
>
{children}
</button>
)
}
// ❌ BAD: No types, unclear structure
export function Button(props) {
return <button onClick={props.onClick}>{props.children}</button>
}
```
### Custom Hooks
```typescript
// ✅ GOOD: Reusable custom hook
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => clearTimeout(handler)
}, [value, delay])
return debouncedValue
}
// Usage
const debouncedQuery = useDebounce(searchQuery, 500)
```
### State Management
```typescript
// ✅ GOOD: Proper state updates
const [count, setCount] = useState(0)
// Functional update for state based on previous state
setCount(prev => prev + 1)
// ❌ BAD: Direct state reference
setCount(count + 1) // Can be stale in async scenarios
```
### Conditional Rendering
```typescript
// ✅ GOOD: Clear conditional rendering
{isLoading && <Spinner />}
{error && <ErrorMessage error={error} />}
{data && <DataDisplay data={data} />}
// ❌ BAD: Ternary hell
{isLoading ? <Spinner /> : error ? <ErrorMessage error={error} /> : data ? <DataDisplay data={data} /> : null}
```
## API Design Standards
### REST API Conventions
```
GET /api/markets # List all markets
GET /api/markets/:id # Get specific market
POST /api/markets # Create new market
PUT /api/markets/:id # Update market (full)
PATCH /api/markets/:id # Update market (partial)
DELETE /api/markets/:id # Delete market
# Query parameters for filtering
GET /api/markets?status=active&limit=10&offset=0
```
### Response Format
```typescript
// ✅ GOOD: Consistent response structure
interface ApiResponse<T> {
success: boolean
data?: T
error?: string
meta?: {
total: number
page: number
limit: number
}
}
// Success response
return { success: true, data: markets, meta: { total: 100, page: 1, limit: 10 } }
// Error response
throw new BadRequestException('Invalid request')
}, { status: 400 })
```
### Input Validation
```typescript
import { IsString, MinLength, IsDateString, IsArray } from 'class-validator'
export class CreateMarketDto {
@IsString()
@MinLength(1)
name: string
@IsString()
@MinLength(1)
description: string
@IsDateString()
endDate: string
@IsArray()
@IsString({ each: true })
categories: string[]
}
@Post()
create(@Body() dto: CreateMarketDto) {
return this.marketsService.create(dto)
}
```
## File Organization
### Project Structure
```
src/
├── main.ts
├── app.module.ts
├── common/ # Filters, guards, pipes, interceptors
├── config/ # Env and Drizzle config
└── modules/ # Feature modules
├── auth/
└── users/
```
### File Naming
```
components/Button.ts # PascalCase for classes
modules/auth/auth.service.ts # NestJS feature files
lib/format-date.ts # kebab/camelCase for utilities
types/market.types.ts # camelCase with .types suffix
```
## Comments & Documentation
### When to Comment
```typescript
// ✅ GOOD: Explain WHY, not WHAT
// Use exponential backoff to avoid overwhelming the API during outages
const delay = Math.min(1000 * Math.pow(2, retryCount), 30000)
// Deliberately using mutation here for performance with large arrays
items.push(newItem)
// ❌ BAD: Stating the obvious
// Increment counter by 1
count++
// Set name to user's name
name = user.name
```
### JSDoc for Public APIs
```typescript
/**
* Searches markets using semantic similarity.
*
* @param query - Natural language search query
* @param limit - Maximum number of results (default: 10)
* @returns Array of markets sorted by similarity score
* @throws {Error} If OpenAI API fails or Redis unavailable
*
* @example
* ```typescript
* const results = await searchMarkets('election', 5)
* console.log(results[0].name) // "Trump vs Biden"
* ```
*/
export async function searchMarkets(
query: string,
limit: number = 10
): Promise<Market[]> {
// Implementation
}
```
## Performance Best Practices
### Memoization
```typescript
import { useMemo, useCallback } from 'react'
// ✅ GOOD: Memoize expensive computations
const sortedMarkets = useMemo(() => {
return markets.sort((a, b) => b.volume - a.volume)
}, [markets])
// ✅ GOOD: Memoize callbacks
const handleSearch = useCallback((query: string) => {
setSearchQuery(query)
}, [])
```
### Lazy Loading
```typescript
import { lazy, Suspense } from 'react'
// ✅ GOOD: Lazy load heavy components
const HeavyChart = lazy(() => import('./HeavyChart'))
export function Dashboard() {
return (
<Suspense fallback={<Spinner />}>
<HeavyChart />
</Suspense>
)
}
```
### Database Queries
```typescript
// ✅ GOOD: Select only needed columns
const { data } = await supabase
.from('markets')
.select('id, name, status')
.limit(10)
// ❌ BAD: Select everything
const { data } = await supabase
.from('markets')
.select('*')
```
## Testing Standards
### Test Structure (AAA Pattern)
```typescript
test('calculates similarity correctly', () => {
// Arrange
const vector1 = [1, 0, 0]
const vector2 = [0, 1, 0]
// Act
const similarity = calculateCosineSimilarity(vector1, vector2)
// Assert
expect(similarity).toBe(0)
})
```
### Test Naming
```typescript
// ✅ GOOD: Descriptive test names
test('returns empty array when no markets match query', () => { })
test('throws error when OpenAI API key is missing', () => { })
test('falls back to substring search when Redis unavailable', () => { })
// ❌ BAD: Vague test names
test('works', () => { })
test('test search', () => { })
```
## Code Smell Detection
Watch for these anti-patterns:
### 1. Long Functions
```typescript
// ❌ BAD: Function > 50 lines
function processMarketData() {
// 100 lines of code
}
// ✅ GOOD: Split into smaller functions
function processMarketData() {
const validated = validateData()
const transformed = transformData(validated)
return saveData(transformed)
}
```
### 2. Deep Nesting
```typescript
// ❌ BAD: 5+ levels of nesting
if (user) {
if (user.isAdmin) {
if (market) {
if (market.isActive) {
if (hasPermission) {
// Do something
}
}
}
}
}
// ✅ GOOD: Early returns
if (!user) return
if (!user.isAdmin) return
if (!market) return
if (!market.isActive) return
if (!hasPermission) return
// Do something
```
### 3. Magic Numbers
```typescript
// ❌ BAD: Unexplained numbers
if (retryCount > 3) { }
setTimeout(callback, 500)
// ✅ GOOD: Named constants
const MAX_RETRIES = 3
const DEBOUNCE_DELAY_MS = 500
if (retryCount > MAX_RETRIES) { }
setTimeout(callback, DEBOUNCE_DELAY_MS)
```
**Remember**: Code quality is not negotiable. Clear, maintainable code enables rapid development and confident refactoring.
@@ -0,0 +1,79 @@
---
name: continuous-learning
description: Automatically extract reusable patterns from Cursor sessions and save them as learned skills for future use.
---
# Continuous Learning Skill
Automatically evaluates Cursor sessions on end to extract reusable patterns that can be saved as learned skills.
## How It Works
This skill runs as a **sessionEnd hook** at the end of each session:
1. **Session Evaluation**: Checks if session has enough messages (default: 10+)
2. **Pattern Detection**: Identifies extractable patterns from the session
3. **Skill Extraction**: Saves useful patterns to `.agents/skills/learned/`
## Configuration
Edit `config.json` to customize:
```json
{
"min_session_length": 10,
"extraction_threshold": "medium",
"auto_approve": false,
"learned_skills_path": ".agents/skills/learned/",
"patterns_to_detect": [
"error_resolution",
"user_corrections",
"workarounds",
"debugging_techniques",
"project_specific"
],
"ignore_patterns": [
"simple_typos",
"one_time_fixes",
"external_api_issues"
]
}
```
## Pattern Types
| Pattern | Description |
|---------|-------------|
| `error_resolution` | How specific errors were resolved |
| `user_corrections` | Patterns from user corrections |
| `workarounds` | Solutions to framework/library quirks |
| `debugging_techniques` | Effective debugging approaches |
| `project_specific` | Project-specific conventions |
## Hook Setup
Already wired in `.cursor/hooks.json` as a `sessionEnd` command:
```json
{
"version": 1,
"hooks": {
"sessionEnd": [
{
"command": "node .cursor/scripts/hooks/evaluate-session.js"
}
]
}
}
```
## Why Stop Hook?
- **Lightweight**: Runs once at session end
- **Non-blocking**: Doesn't add latency to every message
- **Complete context**: Has access to full session transcript
## Related
- [The Longform Guide](https://x.com/affaanmustafa/status/2014040193557471352) - Section on continuous learning
- `/learn` command - Manual pattern extraction mid-session
@@ -0,0 +1,18 @@
{
"min_session_length": 10,
"extraction_threshold": "medium",
"auto_approve": false,
"learned_skills_path": ".agents/skills/learned/",
"patterns_to_detect": [
"error_resolution",
"user_corrections",
"workarounds",
"debugging_techniques",
"project_specific"
],
"ignore_patterns": [
"simple_typos",
"one_time_fixes",
"external_api_issues"
]
}
+53
View File
@@ -0,0 +1,53 @@
#!/bin/bash
# Continuous Learning - Session Evaluator
# Runs on sessionEnd to extract reusable patterns from Cursor sessions
#
# Configured in .cursor/hooks.json:
# sessionEnd -> node .cursor/scripts/hooks/evaluate-session.js
# This shell version is a fallback.
#
# Extracted skills saved to: .agents/skills/learned/
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
CONFIG_FILE="$SCRIPT_DIR/config.json"
LEARNED_SKILLS_PATH="${ROOT}/.agents/skills/learned"
MIN_SESSION_LENGTH=10
# Load config if exists
if [ -f "$CONFIG_FILE" ]; then
MIN_SESSION_LENGTH=$(jq -r '.min_session_length // 10' "$CONFIG_FILE")
configured_path=$(jq -r '.learned_skills_path // empty' "$CONFIG_FILE")
if [ -n "$configured_path" ]; then
LEARNED_SKILLS_PATH=$(echo "$configured_path" | sed "s|~|$HOME|")
case "$LEARNED_SKILLS_PATH" in
/*) ;;
*) LEARNED_SKILLS_PATH="${ROOT}/${LEARNED_SKILLS_PATH}" ;;
esac
fi
fi
# Ensure learned skills directory exists
mkdir -p "$LEARNED_SKILLS_PATH"
# Get transcript path from environment (set by Claude Code)
transcript_path="${CLAUDE_TRANSCRIPT_PATH:-}"
if [ -z "$transcript_path" ] || [ ! -f "$transcript_path" ]; then
exit 0
fi
# Count messages in session
message_count=$(grep -c '"type":"user"' "$transcript_path" 2>/dev/null || echo "0")
# Skip short sessions
if [ "$message_count" -lt "$MIN_SESSION_LENGTH" ]; then
echo "[ContinuousLearning] Session too short ($message_count messages), skipping" >&2
exit 0
fi
# Signal to Claude that session should be evaluated for extractable patterns
echo "[ContinuousLearning] Session has $message_count messages - evaluate for extractable patterns" >&2
echo "[ContinuousLearning] Save learned skills to: $LEARNED_SKILLS_PATH" >&2
+444
View File
@@ -0,0 +1,444 @@
---
name: drizzle
description: 'LobeHub Drizzle ORM schema and query style. Use for pgTable schemas, indexes, joins, inferred types, db.select/db.query, schema fields, foreign keys, junction tables, or postgres query patterns.'
user-invocable: false
---
# Drizzle ORM Schema Style Guide
> **Adding a Model or Repository?** Ship a sibling test in the same PR — every new
> file under `packages/database/src/models/**` or `src/repositories/**` needs a
> matching `__tests__/<name>.test.ts`. See the **testing** skill
> (`.agents/skills/testing/references/db-model-test.md`) for the `getTestDB()`
> integration pattern, user-isolation tests, the BM25 `describe.skipIf(!isServerDB)`
> guard, and schema gotchas. CI's coverage patch gate won't reliably catch a brand-new
> untested file, so this is on you.
## Configuration
- Config: `drizzle.config.ts`
- Schemas: `packages/database/src/schemas/`
- Migrations: `packages/database/migrations/`
- Dialect: `postgresql` with `strict: true`
## Helper Functions
Location: `packages/database/src/schemas/_helpers.ts`
- `timestamptz(name)`: Timestamp with timezone
- `createdAt()`, `updatedAt()`, `accessedAt()`: Standard timestamp columns
- `timestamps`: Object with all three for easy spread
## Naming Conventions
- **Tables**: Plural snake\_case (`users`, `session_groups`)
- **Columns**: snake\_case (`user_id`, `created_at`)
- **New tables**: Check nearby existing tables before naming a new one. Preserve
the established noun family and suffix. For example, if the user-scoped table
is `user_xxx_logs`, the workspace-scoped counterpart should be
`workspace_xxx_logs`, not `workspace_xxx_records` or another new synonym.
```typescript
// ✅ Good: follows the existing user/workspace table family.
export const userSignupLogs = pgTable('user_signup_logs', { ... });
export const workspaceSignupLogs = pgTable('workspace_signup_logs', { ... });
// ❌ Bad: introduces a new suffix for the same concept.
export const workspaceSignupRecords = pgTable('workspace_signup_records', { ... });
```
## Column Definitions
### Primary Keys
Do not use auto-incrementing primary keys (`serial`, `bigserial`, generated
identity columns). They create sequence-state problems during cross-database
migrations, restores, and data copy jobs. Prefer text IDs from application
generators (`idGenerator`, `createNanoId`) or `uuid` for internal tables.
Keep `$defaultFn(...)` when a table normally owns ID generation. Callers can
still pass an explicit `id`; the default only runs when the insert omits it. Do
not remove the default just because one flow needs to supply a request-scoped ID.
```typescript
// ✅ Good: app-generated text ID; explicit inserts can still override it.
id: text('id')
.primaryKey()
.$defaultFn(() => idGenerator('agents'))
.notNull(),
// ❌ Bad: sequence state is fragile across DB migrations and restores.
id: serial('id').primaryKey(),
```
ID prefixes make entity types distinguishable. For internal tables, use `uuid`.
Do not use composite primary keys on new tables. Give every table a single-column
surrogate PK and carry business uniqueness in a `uniqueIndex` instead. PK columns
cannot be nullable, so when the uniqueness scope later grows by a nullable
dimension the composite PK must be torn down and rebuilt — exactly what happened
when `ai_providers` / `ai_models` were workspace-scoped (migration 0110 replaced
their composite PKs with a surrogate `_id` plus partial unique indexes). A unique
index still works as the arbiter for `onConflictDoUpdate` upserts.
```typescript
// ✅ Good: surrogate PK; uniqueness scope can evolve without a PK rebuild.
export const workspaceUserSettings = pgTable(
'workspace_user_settings',
{
id: uuid('id').defaultRandom().notNull().primaryKey(),
workspaceId: text('workspace_id').references(() => workspaces.id, { onDelete: 'cascade' }).notNull(),
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
...timestamps,
},
(t) => [uniqueIndex('workspace_user_settings_workspace_id_user_id_unique').on(t.workspaceId, t.userId)],
);
// ❌ Bad: locked to exactly these columns; adding a nullable scope column
// (workspaceId, deviceId, …) later forces a full PK rebuild migration.
(t) => [primaryKey({ columns: [t.workspaceId, t.userId] })],
```
Existing composite PKs are legacy — leave them alone unless they block a scope
change, then migrate them the 0110 way.
### Foreign Keys
```typescript
userId: text('user_id')
.references(() => users.id, { onDelete: 'cascade' })
.notNull(),
```
### Timestamps
```typescript
...timestamps, // Spread from _helpers.ts
```
### Optional and Undefined Values
Do not introduce artificial sentinel strings for missing values, such as
`unknown`, unless the domain already has that explicit state and existing code
uses it consistently. Prefer nullable columns, optional TypeScript fields, or a
separate concrete status enum when the value is genuinely absent.
```typescript
// ✅ Good: absent until the final stage writes a real decision.
export type UserSignupLogFinalDecision = 'allow' | 'block' | 'error';
finalDecision: varchar('final_decision', { length: 32 }).$type<UserSignupLogFinalDecision>(),
// ❌ Bad: invents a new state that callers now need to handle everywhere.
export type UserSignupLogFinalDecision = 'allow' | 'block' | 'error' | 'unknown';
finalDecision: varchar('final_decision', { length: 32 })
.$type<UserSignupLogFinalDecision>()
.notNull()
.default('unknown');
```
### Database Enums
Default to **not** using PostgreSQL/Drizzle `pgEnum`. Database enums are
expensive to evolve safely: adding members needs migrations, removing or
renaming members is awkward, and deployment order becomes more fragile.
For product/business states, use `text()` or `varchar()` with a TypeScript value
type via `$type<...>()`. Keep those TS-only value types in the domain/shared type
module, then import them into the schema. For cloud DB schemas, that usually
means `cloudDB/types.ts`.
Do not copy existing DB enums as a pattern. Treat them as legacy or explicitly
reviewed exceptions. If a new `pgEnum` seems necessary, stop and justify why the
value set is effectively immutable and why the migration cost is acceptable.
### Field Descriptions
For columns whose meaning is not obvious from the name alone, add JSDoc on the
schema field. Include a concrete example when it clarifies the stored value or
the lifecycle moment that writes it. This is especially important for external
IDs, lifecycle statuses, denormalized snapshots, JSONB signals, and fields whose
name could mean either a request ID or a persisted row ID.
```typescript
// ✅ Good: explain the table's business object first, then only document
// non-obvious lifecycle or risk-control fields.
/**
* User signup logs - one row per signup flow, collecting stage-level
* risk-control decisions before and after the auth provider creates a user.
*/
export const userSignupLogs = pgTable('user_signup_logs', {
/** Final signup outcome reason, for example user_created, llm_block, or guard_error */
finalReason: text('final_reason'),
/** Aggregated risk level derived from stage decisions, for example block -> high */
riskLevel: varchar('risk_level', { length: 16 }).$type<UserSignupLogRiskLevel>(),
/** Ordered stage-level decisions and metadata grouped by signup review stage */
stageResults: jsonb('stage_results').$type<UserSignupLogStageResults>(),
});
// ❌ Bad: comments restate obvious column names without adding domain meaning.
/** User email */
email: text('email'),
```
### JSONB Types
Avoid `Record<string, unknown>` or similarly loose JSONB types for schema
columns. Define a concrete interface that describes the expected JSON shape, even
when most properties are optional. This keeps callers, migrations, and review
queries aligned on the same data contract.
```typescript
interface UserSignupLogMetadata {
payloadPath?: string;
requestPath?: string;
}
metadata: jsonb('metadata').$type<UserSignupLogMetadata>(),
```
```typescript
// ❌ Bad: hides the contract and makes downstream access untyped.
metadata: jsonb('metadata').$type<Record<string, unknown>>(),
```
A loosely-typed JSONB column is often a symptom of a deeper problem: the column
was reserved speculatively ("for future extension") and nothing actually writes
it. Don't add `metadata` / `extra` JSONB columns for hypothetical future needs —
a column earns its place only when a concrete writer ships alongside it. When
review finds such a column, the fix is to **delete the column**, not to invent
an interface for data that doesn't exist; add a properly-typed column once the
real requirement arrives.
### Indexes
```typescript
// Return array (object style deprecated)
(t) => [uniqueIndex('client_id_user_id_unique').on(t.clientId, t.userId)],
```
## Type Inference
```typescript
export const insertAgentSchema = createInsertSchema(agents);
export type NewAgent = typeof agents.$inferInsert;
export type AgentItem = typeof agents.$inferSelect;
```
## Example Pattern
```typescript
export const agents = pgTable(
'agents',
{
id: text('id')
.primaryKey()
.$defaultFn(() => idGenerator('agents'))
.notNull(),
slug: varchar('slug', { length: 100 })
.$defaultFn(() => randomSlug(4))
.unique(),
userId: text('user_id')
.references(() => users.id, { onDelete: 'cascade' })
.notNull(),
clientId: text('client_id'),
chatConfig: jsonb('chat_config').$type<LobeAgentChatConfig>(),
...timestamps,
},
(t) => [uniqueIndex('client_id_user_id_unique').on(t.clientId, t.userId)],
);
```
## Common Patterns
### Junction Tables (Many-to-Many)
The surrogate-PK rule above applies to junction tables too — pair uniqueness
goes in a `uniqueIndex`, not a composite PK (many existing junction tables
still use composite PKs; that is legacy, not the template):
```typescript
export const agentsKnowledgeBases = pgTable(
'agents_knowledge_bases',
{
id: uuid('id').defaultRandom().notNull().primaryKey(),
agentId: text('agent_id')
.references(() => agents.id, { onDelete: 'cascade' })
.notNull(),
knowledgeBaseId: text('knowledge_base_id')
.references(() => knowledgeBases.id, { onDelete: 'cascade' })
.notNull(),
userId: text('user_id')
.references(() => users.id, { onDelete: 'cascade' })
.notNull(),
enabled: boolean('enabled').default(true),
...timestamps,
},
(t) => [
uniqueIndex('agents_knowledge_bases_agent_id_knowledge_base_id_unique').on(
t.agentId,
t.knowledgeBaseId,
),
],
);
```
## Query Style
**Always use `db.select()` builder API. Never use `db.query.*` relational API** (`findMany`, `findFirst`, `with:`).
The relational API generates complex lateral joins with `json_build_array` that are fragile and hard to debug.
### Select Single Row
```typescript
// ✅ Good
const [result] = await this.db.select().from(agents).where(eq(agents.id, id)).limit(1);
return result;
// ❌ Bad: relational API
return this.db.query.agents.findFirst({
where: eq(agents.id, id),
});
```
### Select with JOIN
```typescript
// ✅ Good: explicit select + leftJoin
const rows = await this.db
.select({
runId: agentEvalRunTopics.runId,
score: agentEvalRunTopics.score,
testCase: agentEvalTestCases,
topic: topics,
})
.from(agentEvalRunTopics)
.leftJoin(agentEvalTestCases, eq(agentEvalRunTopics.testCaseId, agentEvalTestCases.id))
.leftJoin(topics, eq(agentEvalRunTopics.topicId, topics.id))
.where(eq(agentEvalRunTopics.runId, runId))
.orderBy(asc(agentEvalRunTopics.createdAt));
// ❌ Bad: relational API with `with:`
return this.db.query.agentEvalRunTopics.findMany({
where: eq(agentEvalRunTopics.runId, runId),
with: { testCase: true, topic: true },
});
```
### Select with Aggregation
```typescript
// ✅ Good: select + leftJoin + groupBy
const rows = await this.db
.select({
id: agentEvalDatasets.id,
name: agentEvalDatasets.name,
testCaseCount: count(agentEvalTestCases.id).as('testCaseCount'),
})
.from(agentEvalDatasets)
.leftJoin(agentEvalTestCases, eq(agentEvalDatasets.id, agentEvalTestCases.datasetId))
.groupBy(agentEvalDatasets.id);
```
### Raw SQL and Advanced Queries
Prefer Drizzle builders whenever the query reads clearly with `select`,
`insert().select()`, `update().from()`, joins, CTEs, and `groupBy` — this keeps
table/column references tied to schema, so changes surface as TypeScript errors.
Within a builder, expression-level `sql<T>` is fine for features lacking a helper
(JSON path, casts, aggregates, `CASE`, `NOW()`). Row locks are clauses, not
expressions — use `.for('update')`, never raw `FOR UPDATE`.
Use `COALESCE` only when null-handling is part of required DB semantics (nullable
JSONB append/merge, "keep first non-null"). Don't scatter
`COALESCE(excluded.col, current.col)` across ordinary upsert scalars just to avoid
an update object — build `set` from defined values only, and hide any remaining
SQL behind named helpers (`appendJsonbArray`, `mergeJsonbObject`, `keepFirstValue`)
so the method reads as business intent, not SQL plumbing.
```typescript
// ✅ Scalars included only when present; SQL hidden behind a named helper.
const updateValues = compactUndefined({
email: record.email ?? undefined,
ip: record.ip ?? undefined,
});
await db.insert(userSignupLogs).values(values).onConflictDoUpdate({
set: { ...updateValues, stageResults: appendStageResult(stage, result), updatedAt: now },
target: userSignupLogs.id,
});
// ❌ Every scalar becomes SQL plumbing.
set: {
email: sql`COALESCE(excluded.email, ${userSignupLogs.email})`,
ip: sql`COALESCE(excluded.ip, ${userSignupLogs.ip})`,
}
```
When refactoring raw SQL:
- Preserve query shape on latency-sensitive paths. If raw SQL is one roundtrip,
don't split it into multiple depth-based queries just to drop `execute`.
- Use `$with(...)` + `insert().select()` / `update().from()` for multi-step
single-roundtrip writes Drizzle can express.
- Don't rely on `execute<MyRow>(sql...)` for safety — it types rows but doesn't keep
selected columns in sync with schema changes.
- If only a PostgreSQL feature Drizzle can't express works, keep the raw SQL and
tighten it: schema refs in interpolations, explicit user scope, a narrow row
interface, and regression tests.
Recursive CTEs are the canonical "keep raw" case — there's no clean `WITH RECURSIVE`
builder, and a rewrite would add depth-based roundtrips:
```typescript
interface TaskTreeRow {
id: string;
parent_task_id: string | null;
}
// execute<T> acceptable: no clean WITH RECURSIVE builder. Keep schema refs in the
// interpolations and scope every leg to the user.
const { rows } = await db.execute<TaskTreeRow>(sql`
WITH RECURSIVE task_tree AS (
SELECT ${tasks.id}, ${tasks.parentTaskId}
FROM ${tasks}
WHERE ${tasks.id} = ${rootTaskId} AND ${tasks.createdByUserId} = ${userId}
UNION ALL
SELECT ${tasks.id}, ${tasks.parentTaskId}
FROM ${tasks}
JOIN task_tree ON ${tasks.parentTaskId} = task_tree.id
WHERE ${tasks.createdByUserId} = ${userId}
)
SELECT * FROM task_tree
`);
```
### One-to-Many (Separate Queries)
When you need a parent record with its children, use two queries instead of relational `with:`:
```typescript
// ✅ Good: two simple queries
const [dataset] = await this.db
.select()
.from(agentEvalDatasets)
.where(eq(agentEvalDatasets.id, id))
.limit(1);
if (!dataset) return undefined;
const testCases = await this.db
.select()
.from(agentEvalTestCases)
.where(eq(agentEvalTestCases.datasetId, id))
.orderBy(asc(agentEvalTestCases.sortOrder));
return { ...dataset, testCases };
```
## Database Migrations
See the `db-migrations` skill for the detailed migration guide.
+221
View File
@@ -0,0 +1,221 @@
# Eval Harness Skill
A formal evaluation framework for Claude Code sessions, implementing eval-driven development (EDD) principles.
## Philosophy
Eval-Driven Development treats evals as the "unit tests of AI development":
- Define expected behavior BEFORE implementation
- Run evals continuously during development
- Track regressions with each change
- Use pass@k metrics for reliability measurement
## Eval Types
### Capability Evals
Test if Claude can do something it couldn't before:
```markdown
[CAPABILITY EVAL: feature-name]
Task: Description of what Claude should accomplish
Success Criteria:
- [ ] Criterion 1
- [ ] Criterion 2
- [ ] Criterion 3
Expected Output: Description of expected result
```
### Regression Evals
Ensure changes don't break existing functionality:
```markdown
[REGRESSION EVAL: feature-name]
Baseline: SHA or checkpoint name
Tests:
- existing-test-1: PASS/FAIL
- existing-test-2: PASS/FAIL
- existing-test-3: PASS/FAIL
Result: X/Y passed (previously Y/Y)
```
## Grader Types
### 1. Code-Based Grader
Deterministic checks using code:
```bash
# Check if file contains expected pattern
grep -q "export function handleAuth" src/auth.ts && echo "PASS" || echo "FAIL"
# Check if tests pass
npm test -- --testPathPattern="auth" && echo "PASS" || echo "FAIL"
# Check if build succeeds
npm run build && echo "PASS" || echo "FAIL"
```
### 2. Model-Based Grader
Use Claude to evaluate open-ended outputs:
```markdown
[MODEL GRADER PROMPT]
Evaluate the following code change:
1. Does it solve the stated problem?
2. Is it well-structured?
3. Are edge cases handled?
4. Is error handling appropriate?
Score: 1-5 (1=poor, 5=excellent)
Reasoning: [explanation]
```
### 3. Human Grader
Flag for manual review:
```markdown
[HUMAN REVIEW REQUIRED]
Change: Description of what changed
Reason: Why human review is needed
Risk Level: LOW/MEDIUM/HIGH
```
## Metrics
### pass@k
"At least one success in k attempts"
- pass@1: First attempt success rate
- pass@3: Success within 3 attempts
- Typical target: pass@3 > 90%
### pass^k
"All k trials succeed"
- Higher bar for reliability
- pass^3: 3 consecutive successes
- Use for critical paths
## Eval Workflow
### 1. Define (Before Coding)
```markdown
## EVAL DEFINITION: feature-xyz
### Capability Evals
1. Can create new user account
2. Can validate email format
3. Can hash password securely
### Regression Evals
1. Existing login still works
2. Session management unchanged
3. Logout flow intact
### Success Metrics
- pass@3 > 90% for capability evals
- pass^3 = 100% for regression evals
```
### 2. Implement
Write code to pass the defined evals.
### 3. Evaluate
```bash
# Run capability evals
[Run each capability eval, record PASS/FAIL]
# Run regression evals
npm test -- --testPathPattern="existing"
# Generate report
```
### 4. Report
```markdown
EVAL REPORT: feature-xyz
========================
Capability Evals:
create-user: PASS (pass@1)
validate-email: PASS (pass@2)
hash-password: PASS (pass@1)
Overall: 3/3 passed
Regression Evals:
login-flow: PASS
session-mgmt: PASS
logout-flow: PASS
Overall: 3/3 passed
Metrics:
pass@1: 67% (2/3)
pass@3: 100% (3/3)
Status: READY FOR REVIEW
```
## Integration Patterns
### Pre-Implementation
```
/eval define feature-name
```
Creates eval definition file at `.cursor/evals/feature-name.md`
### During Implementation
```
/eval check feature-name
```
Runs current evals and reports status
### Post-Implementation
```
/eval report feature-name
```
Generates full eval report
## Eval Storage
Store evals in project:
```
.cursor/
evals/
feature-xyz.md # Eval definition
feature-xyz.log # Eval run history
baseline.json # Regression baselines
```
## Best Practices
1. **Define evals BEFORE coding** - Forces clear thinking about success criteria
2. **Run evals frequently** - Catch regressions early
3. **Track pass@k over time** - Monitor reliability trends
4. **Use code graders when possible** - Deterministic > probabilistic
5. **Human review for security** - Never fully automate security checks
6. **Keep evals fast** - Slow evals don't get run
7. **Version evals with code** - Evals are first-class artifacts
## Example: Adding Authentication
```markdown
## EVAL: add-authentication
### Phase 1: Define (10 min)
Capability Evals:
- [ ] User can register with email/password
- [ ] User can login with valid credentials
- [ ] Invalid credentials rejected with proper error
- [ ] Sessions persist across page reloads
- [ ] Logout clears session
Regression Evals:
- [ ] Public routes still accessible
- [ ] API responses unchanged
- [ ] Database schema compatible
### Phase 2: Implement (varies)
[Write code]
### Phase 3: Evaluate
Run: /eval check add-authentication
### Phase 4: Report
EVAL REPORT: add-authentication
==============================
Capability: 5/5 passed (pass@3: 100%)
Regression: 3/3 passed (pass^3: 100%)
Status: SHIP IT
```
@@ -0,0 +1,131 @@
---
name: nestjs-best-practices
description: NestJS best practices and architecture patterns for building production-ready applications. This skill should be used when writing, reviewing, or refactoring NestJS code to ensure proper patterns for modules, dependency injection, security, and performance.
license: MIT
metadata:
author: Kadajett
version: "1.2.0"
---
# NestJS Best Practices
When a rule mentions TypeORM, apply the same idea with **Drizzle ORM** (`drizzle/schema.ts` and `drizzle-kit` migrations). This project does not use TypeORM.
## When to Apply
Reference these guidelines when:
- Writing new NestJS modules, controllers, or services
- Implementing authentication and authorization
- Reviewing code for architecture and security issues
- Refactoring existing NestJS codebases
- Optimizing performance or database queries
- Building microservices architectures
## Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|----------|----------|--------|--------|
| 1 | Architecture | CRITICAL | `arch-` |
| 2 | Dependency Injection | CRITICAL | `di-` |
| 3 | Error Handling | HIGH | `error-` |
| 4 | Security | HIGH | `security-` |
| 5 | Performance | HIGH | `perf-` |
| 6 | Testing | MEDIUM-HIGH | `test-` |
| 7 | Database & ORM | MEDIUM-HIGH | `db-` |
| 8 | API Design | MEDIUM | `api-` |
| 9 | Microservices | MEDIUM | `micro-` |
| 10 | DevOps & Deployment | LOW-MEDIUM | `devops-` |
## Quick Reference
### 1. Architecture (CRITICAL)
- `arch-avoid-circular-deps` - Avoid circular module dependencies
- `arch-feature-modules` - Organize by feature, not technical layer
- `arch-module-sharing` - Proper module exports/imports, avoid duplicate providers
- `arch-single-responsibility` - Focused services over "god services"
- `arch-use-repository-pattern` - Abstract database logic for testability
- `arch-use-events` - Event-driven architecture for decoupling
### 2. Dependency Injection (CRITICAL)
- `di-avoid-service-locator` - Avoid service locator anti-pattern
- `di-interface-segregation` - Interface Segregation Principle (ISP)
- `di-liskov-substitution` - Liskov Substitution Principle (LSP)
- `di-prefer-constructor-injection` - Constructor over property injection
- `di-scope-awareness` - Understand singleton/request/transient scopes
- `di-use-interfaces-tokens` - Use injection tokens for interfaces
### 3. Error Handling (HIGH)
- `error-use-exception-filters` - Centralized exception handling
- `error-throw-http-exceptions` - Use NestJS HTTP exceptions
- `error-handle-async-errors` - Handle async errors properly
### 4. Security (HIGH)
- `security-auth-jwt` - Secure JWT authentication
- `security-validate-all-input` - Validate with class-validator
- `security-use-guards` - Authentication and authorization guards
- `security-sanitize-output` - Prevent XSS attacks
- `security-rate-limiting` - Implement rate limiting
### 5. Performance (HIGH)
- `perf-async-hooks` - Proper async lifecycle hooks
- `perf-use-caching` - Implement caching strategies
- `perf-optimize-database` - Optimize database queries
- `perf-lazy-loading` - Lazy load modules for faster startup
### 6. Testing (MEDIUM-HIGH)
- `test-use-testing-module` - Use NestJS testing utilities
- `test-e2e-supertest` - E2E testing with Supertest
- `test-mock-external-services` - Mock external dependencies
### 7. Database & ORM (MEDIUM-HIGH)
- `db-use-transactions` - Transaction management
- `db-avoid-n-plus-one` - Avoid N+1 query problems
- `db-use-migrations` - Use migrations for schema changes
### 8. API Design (MEDIUM)
- `api-use-dto-serialization` - DTO and response serialization
- `api-use-interceptors` - Cross-cutting concerns
- `api-versioning` - API versioning strategies
- `api-use-pipes` - Input transformation with pipes
### 9. Microservices (MEDIUM)
- `micro-use-patterns` - Message and event patterns
- `micro-use-health-checks` - Health checks for orchestration
- `micro-use-queues` - Background job processing
### 10. DevOps & Deployment (LOW-MEDIUM)
- `devops-use-config-module` - Environment configuration
- `devops-use-logging` - Structured logging
- `devops-graceful-shutdown` - Zero-downtime deployments
## How to Use
Read individual rule files for detailed explanations and code examples:
```
rules/arch-avoid-circular-deps.md
rules/security-validate-all-input.md
rules/_sections.md
```
Each rule file contains:
- Brief explanation of why it matters
- Incorrect code example with explanation
- Correct code example with explanation
- Additional context and references
## Full Compiled Document
For the complete guide with all rules expanded in a single document, see
[AGENTS.md in the repository](https://github.com/Kadajett/agent-nestjs-skills/blob/main/AGENTS.md).
@@ -0,0 +1,56 @@
# Sections
This file defines all sections, their ordering, impact levels, and descriptions.
The section ID (in parentheses) is the filename prefix used to group rules.
---
## 1. Architecture (arch)
**Impact:** CRITICAL
**Description:** Proper module organization and dependency management are the foundation of maintainable NestJS applications. Circular dependencies and god services are the #1 architecture killer.
## 2. Dependency Injection (di)
**Impact:** CRITICAL
**Description:** NestJS's IoC container is powerful but can be misused. Understanding scopes, injection tokens, and proper patterns is essential for testable code.
## 3. Error Handling (error)
**Impact:** HIGH
**Description:** Consistent error handling improves debugging, user experience, and API reliability. Centralized exception filters ensure uniform error responses.
## 4. Security (security)
**Impact:** HIGH
**Description:** Security vulnerabilities can be catastrophic. Input validation, authentication, authorization, and data protection are non-negotiable.
## 5. Performance (perf)
**Impact:** HIGH
**Description:** Optimizing request handling, caching, and database queries directly impacts application responsiveness and scalability.
## 6. Testing (test)
**Impact:** MEDIUM-HIGH
**Description:** Well-tested applications are more reliable. NestJS testing utilities enable comprehensive unit and e2e coverage.
## 7. Database & ORM (db)
**Impact:** MEDIUM-HIGH
**Description:** Proper database access patterns, transactions, and query optimization are crucial for data-intensive applications.
## 8. API Design (api)
**Impact:** MEDIUM
**Description:** RESTful conventions, versioning, DTOs, and consistent response formats improve API usability and maintainability.
## 9. Microservices (micro)
**Impact:** MEDIUM
**Description:** Building distributed systems requires understanding message patterns, health checks, and inter-service communication.
## 10. DevOps & Deployment (devops)
**Impact:** LOW-MEDIUM
**Description:** Configuration management, structured logging, and graceful shutdown ensure production readiness and zero-downtime deployments.
@@ -0,0 +1,28 @@
---
title: Rule Title Here
impact: MEDIUM
impactDescription: Optional description of impact (e.g., "20-50% improvement")
tags: tag1, tag2
---
## Rule Title Here
**Impact: MEDIUM (optional impact description)**
Brief explanation of the rule and why it matters. This should be clear and concise, explaining the performance implications.
**Incorrect (description of what's wrong):**
```typescript
// Bad code example here
const bad = example()
```
**Correct (description of what's right):**
```typescript
// Good code example here
const good = example()
```
Reference: [Link to documentation or resource](https://example.com)
@@ -0,0 +1,182 @@
---
title: Use DTOs and Serialization for API Responses
impact: MEDIUM
impactDescription: Response DTOs prevent accidental data exposure and ensure consistency
tags: api, dto, serialization, class-transformer
---
## Use DTOs and Serialization for API Responses
Never return entity objects directly from controllers. Use response DTOs with class-transformer's `@Exclude()` and `@Expose()` decorators to control exactly what data is sent to clients. This prevents accidental exposure of sensitive fields and provides a stable API contract.
**Incorrect (returning entities directly or manual spreading):**
```typescript
// Return entities directly
@Controller('users')
export class UsersController {
@Get(':id')
async findOne(@Param('id') id: string): Promise<User> {
return this.usersService.findById(id);
// Returns: { id, email, passwordHash, ssn, internalNotes, ... }
// Exposes sensitive data!
}
}
// Manual object spreading (error-prone)
@Get(':id')
async findOne(@Param('id') id: string) {
const user = await this.usersService.findById(id);
return {
id: user.id,
email: user.email,
name: user.name,
// Easy to forget to exclude sensitive fields
// Hard to maintain across endpoints
};
}
```
**Correct (use class-transformer with @Exclude and response DTOs):**
```typescript
// Enable class-transformer globally
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
await app.listen(3000);
}
// Entity with serialization control
@Entity()
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
email: string;
@Column()
name: string;
@Column()
@Exclude() // Never include in responses
passwordHash: string;
@Column({ nullable: true })
@Exclude()
ssn: string;
@Column({ default: false })
@Exclude({ toPlainOnly: true }) // Exclude from response, allow in requests
isAdmin: boolean;
@CreateDateColumn()
createdAt: Date;
@Column()
@Exclude()
internalNotes: string;
}
// Now returning entity is safe
@Controller('users')
export class UsersController {
@Get(':id')
async findOne(@Param('id') id: string): Promise<User> {
return this.usersService.findById(id);
// Returns: { id, email, name, createdAt }
// Sensitive fields excluded automatically
}
}
// For different response shapes, use explicit DTOs
export class UserResponseDto {
@Expose()
id: string;
@Expose()
email: string;
@Expose()
name: string;
@Expose()
@Transform(({ obj }) => obj.posts?.length || 0)
postCount: number;
constructor(partial: Partial<User>) {
Object.assign(this, partial);
}
}
export class UserDetailResponseDto extends UserResponseDto {
@Expose()
createdAt: Date;
@Expose()
@Type(() => PostResponseDto)
posts: PostResponseDto[];
}
// Controller with explicit DTOs
@Controller('users')
export class UsersController {
@Get()
@SerializeOptions({ type: UserResponseDto })
async findAll(): Promise<UserResponseDto[]> {
const users = await this.usersService.findAll();
return users.map(u => plainToInstance(UserResponseDto, u));
}
@Get(':id')
async findOne(@Param('id') id: string): Promise<UserDetailResponseDto> {
const user = await this.usersService.findByIdWithPosts(id);
return plainToInstance(UserDetailResponseDto, user, {
excludeExtraneousValues: true,
});
}
}
// Groups for conditional serialization
export class UserDto {
@Expose()
id: string;
@Expose()
name: string;
@Expose({ groups: ['admin'] })
email: string;
@Expose({ groups: ['admin'] })
createdAt: Date;
@Expose({ groups: ['admin', 'owner'] })
settings: UserSettings;
}
@Controller('users')
export class UsersController {
@Get()
@SerializeOptions({ groups: ['public'] })
async findAllPublic(): Promise<UserDto[]> {
// Returns: { id, name }
}
@Get('admin')
@UseGuards(AdminGuard)
@SerializeOptions({ groups: ['admin'] })
async findAllAdmin(): Promise<UserDto[]> {
// Returns: { id, name, email, createdAt }
}
@Get('me')
@SerializeOptions({ groups: ['owner'] })
async getProfile(@CurrentUser() user: User): Promise<UserDto> {
// Returns: { id, name, settings }
}
}
```
Reference: [NestJS Serialization](https://docs.nestjs.com/techniques/serialization)
@@ -0,0 +1,202 @@
---
title: Use Interceptors for Cross-Cutting Concerns
impact: MEDIUM-HIGH
impactDescription: Interceptors provide clean separation for cross-cutting logic
tags: api, interceptors, logging, caching
---
## Use Interceptors for Cross-Cutting Concerns
Interceptors can transform responses, add logging, handle caching, and measure performance without polluting your business logic. They wrap the route handler execution, giving you access to both the request and response streams.
**Incorrect (logging and transformation in every method):**
```typescript
// Logging in every controller method
@Controller('users')
export class UsersController {
@Get()
async findAll(): Promise<User[]> {
const start = Date.now();
this.logger.log('findAll called');
const users = await this.usersService.findAll();
this.logger.log(`findAll completed in ${Date.now() - start}ms`);
return users;
}
@Get(':id')
async findOne(@Param('id') id: string): Promise<User> {
const start = Date.now();
this.logger.log(`findOne called with id: ${id}`);
const user = await this.usersService.findOne(id);
this.logger.log(`findOne completed in ${Date.now() - start}ms`);
return user;
}
// Repeated in every method!
}
// Manual response wrapping
@Get()
async findAll(): Promise<{ data: User[]; meta: Meta }> {
const users = await this.usersService.findAll();
return {
data: users,
meta: { timestamp: new Date(), count: users.length },
};
}
```
**Correct (use interceptors for cross-cutting concerns):**
```typescript
// Logging interceptor
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger('HTTP');
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const { method, url, body } = request;
const now = Date.now();
return next.handle().pipe(
tap({
next: (data) => {
const response = context.switchToHttp().getResponse();
this.logger.log(
`${method} ${url} ${response.statusCode} - ${Date.now() - now}ms`,
);
},
error: (error) => {
this.logger.error(
`${method} ${url} ${error.status || 500} - ${Date.now() - now}ms`,
error.stack,
);
},
}),
);
}
}
// Response transformation interceptor
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
return next.handle().pipe(
map((data) => ({
data,
meta: {
timestamp: new Date().toISOString(),
path: context.switchToHttp().getRequest().url,
},
})),
);
}
}
// Timeout interceptor
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(5000),
catchError((err) => {
if (err instanceof TimeoutError) {
throw new RequestTimeoutException('Request timed out');
}
throw err;
}),
);
}
}
// Apply globally or per-controller
@Module({
providers: [
{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },
{ provide: APP_INTERCEPTOR, useClass: TransformInterceptor },
],
})
export class AppModule {}
// Or per-controller
@Controller('users')
@UseInterceptors(LoggingInterceptor)
export class UsersController {
@Get()
async findAll(): Promise<User[]> {
// Clean business logic only
return this.usersService.findAll();
}
}
// Custom cache interceptor with TTL
@Injectable()
export class HttpCacheInterceptor implements NestInterceptor {
constructor(
private cacheManager: Cache,
private reflector: Reflector,
) {}
async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
const request = context.switchToHttp().getRequest();
// Only cache GET requests
if (request.method !== 'GET') {
return next.handle();
}
const cacheKey = this.generateKey(request);
const ttl = this.reflector.get<number>('cacheTTL', context.getHandler()) || 300;
const cached = await this.cacheManager.get(cacheKey);
if (cached) {
return of(cached);
}
return next.handle().pipe(
tap((response) => {
this.cacheManager.set(cacheKey, response, ttl);
}),
);
}
private generateKey(request: Request): string {
return `cache:${request.url}:${JSON.stringify(request.query)}`;
}
}
// Usage with custom TTL
@Get()
@SetMetadata('cacheTTL', 600)
@UseInterceptors(HttpCacheInterceptor)
async findAll(): Promise<User[]> {
return this.usersService.findAll();
}
// Error mapping interceptor
@Injectable()
export class ErrorMappingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
catchError((error) => {
if (error instanceof EntityNotFoundError) {
throw new NotFoundException(error.message);
}
if (error instanceof QueryFailedError) {
if (error.message.includes('duplicate')) {
throw new ConflictException('Resource already exists');
}
}
throw error;
}),
);
}
}
```
Reference: [NestJS Interceptors](https://docs.nestjs.com/interceptors)
@@ -0,0 +1,205 @@
---
title: Use Pipes for Input Transformation
impact: MEDIUM
impactDescription: Pipes ensure clean, validated data reaches your handlers
tags: api, pipes, validation, transformation
---
## Use Pipes for Input Transformation
Use built-in pipes like `ParseIntPipe`, `ParseUUIDPipe`, and `DefaultValuePipe` for common transformations. Create custom pipes for business-specific transformations. Pipes separate validation/transformation logic from controllers.
**Incorrect (manual type parsing in handlers):**
```typescript
// Manual type parsing in handlers
@Controller('users')
export class UsersController {
@Get(':id')
async findOne(@Param('id') id: string): Promise<User> {
// Manual validation in every handler
const uuid = id.trim();
if (!isUUID(uuid)) {
throw new BadRequestException('Invalid UUID');
}
return this.usersService.findOne(uuid);
}
@Get()
async findAll(
@Query('page') page: string,
@Query('limit') limit: string,
): Promise<User[]> {
// Manual parsing and defaults
const pageNum = parseInt(page) || 1;
const limitNum = parseInt(limit) || 10;
return this.usersService.findAll(pageNum, limitNum);
}
}
// Type coercion without validation
@Get()
async search(@Query('price') price: string): Promise<Product[]> {
const priceNum = +price; // NaN if invalid, no error
return this.productsService.findByPrice(priceNum);
}
```
**Correct (use built-in and custom pipes):**
```typescript
// Use built-in pipes for common transformations
@Controller('users')
export class UsersController {
@Get(':id')
async findOne(@Param('id', ParseUUIDPipe) id: string): Promise<User> {
// id is guaranteed to be a valid UUID
return this.usersService.findOne(id);
}
@Get()
async findAll(
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
): Promise<User[]> {
// Automatic defaults and type conversion
return this.usersService.findAll(page, limit);
}
@Get('by-status/:status')
async findByStatus(
@Param('status', new ParseEnumPipe(UserStatus)) status: UserStatus,
): Promise<User[]> {
return this.usersService.findByStatus(status);
}
}
// Custom pipe for business logic
@Injectable()
export class ParseDatePipe implements PipeTransform<string, Date> {
transform(value: string): Date {
const date = new Date(value);
if (isNaN(date.getTime())) {
throw new BadRequestException('Invalid date format');
}
return date;
}
}
@Get('reports')
async getReports(
@Query('from', ParseDatePipe) from: Date,
@Query('to', ParseDatePipe) to: Date,
): Promise<Report[]> {
return this.reportsService.findBetween(from, to);
}
// Custom transformation pipes
@Injectable()
export class NormalizeEmailPipe implements PipeTransform<string, string> {
transform(value: string): string {
if (!value) return value;
return value.trim().toLowerCase();
}
}
// Parse comma-separated values
@Injectable()
export class ParseArrayPipe implements PipeTransform<string, string[]> {
transform(value: string): string[] {
if (!value) return [];
return value.split(',').map((v) => v.trim()).filter(Boolean);
}
}
@Get('products')
async findProducts(
@Query('ids', ParseArrayPipe) ids: string[],
@Query('email', NormalizeEmailPipe) email: string,
): Promise<Product[]> {
// ids is already an array, email is normalized
return this.productsService.findByIds(ids);
}
// Sanitize HTML input
@Injectable()
export class SanitizeHtmlPipe implements PipeTransform<string, string> {
transform(value: string): string {
if (!value) return value;
return sanitizeHtml(value, { allowedTags: [] });
}
}
// Global validation pipe with transformation
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // Strip non-DTO properties
transform: true, // Auto-transform to DTO types
transformOptions: {
enableImplicitConversion: true, // Convert query strings to numbers
},
forbidNonWhitelisted: true, // Throw on extra properties
}),
);
// DTO with transformation decorators
export class FindProductsDto {
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page?: number = 1;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(100)
limit?: number = 10;
@IsOptional()
@Transform(({ value }) => value?.toLowerCase())
@IsString()
search?: string;
@IsOptional()
@Transform(({ value }) => value?.split(','))
@IsArray()
@IsString({ each: true })
categories?: string[];
}
@Get()
async findAll(@Query() dto: FindProductsDto): Promise<Product[]> {
// dto is already transformed and validated
return this.productsService.findAll(dto);
}
// Pipe error customization
@Injectable()
export class CustomParseIntPipe extends ParseIntPipe {
constructor() {
super({
exceptionFactory: (error) =>
new BadRequestException(`${error} must be a valid integer`),
});
}
}
// Or use options on built-in pipes
@Get(':id')
async findOne(
@Param(
'id',
new ParseIntPipe({
errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE,
exceptionFactory: () => new NotAcceptableException('ID must be numeric'),
}),
)
id: number,
): Promise<Item> {
return this.itemsService.findOne(id);
}
```
Reference: [NestJS Pipes](https://docs.nestjs.com/pipes)
@@ -0,0 +1,191 @@
---
title: Use API Versioning for Breaking Changes
impact: MEDIUM
impactDescription: Versioning allows you to evolve APIs without breaking existing clients
tags: api, versioning, breaking-changes, compatibility
---
## Use API Versioning for Breaking Changes
Use NestJS built-in versioning when making breaking changes to your API. Choose a versioning strategy (URI, header, or media type) and apply it consistently. This allows old clients to continue working while new clients use updated endpoints.
**Incorrect (breaking changes without versioning):**
```typescript
// Breaking changes without versioning
@Controller('users')
export class UsersController {
@Get(':id')
async findOne(@Param('id') id: string): Promise<User> {
// Original response: { id, name, email }
// Later changed to: { id, firstName, lastName, emailAddress }
// Old clients break!
return this.usersService.findOne(id);
}
}
// Manual versioning in routes
@Controller('v1/users')
export class UsersV1Controller {}
@Controller('v2/users')
export class UsersV2Controller {}
// Inconsistent, error-prone, hard to maintain
```
**Correct (use NestJS built-in versioning):**
```typescript
// Enable versioning in main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// URI versioning: /v1/users, /v2/users
app.enableVersioning({
type: VersioningType.URI,
defaultVersion: '1',
});
// Or header versioning: X-API-Version: 1
app.enableVersioning({
type: VersioningType.HEADER,
header: 'X-API-Version',
defaultVersion: '1',
});
// Or media type: Accept: application/json;v=1
app.enableVersioning({
type: VersioningType.MEDIA_TYPE,
key: 'v=',
defaultVersion: '1',
});
await app.listen(3000);
}
// Version-specific controllers
@Controller('users')
@Version('1')
export class UsersV1Controller {
@Get(':id')
async findOne(@Param('id') id: string): Promise<UserV1Response> {
const user = await this.usersService.findOne(id);
// V1 response format
return {
id: user.id,
name: user.name,
email: user.email,
};
}
}
@Controller('users')
@Version('2')
export class UsersV2Controller {
@Get(':id')
async findOne(@Param('id') id: string): Promise<UserV2Response> {
const user = await this.usersService.findOne(id);
// V2 response format with breaking changes
return {
id: user.id,
firstName: user.firstName,
lastName: user.lastName,
emailAddress: user.email,
createdAt: user.createdAt,
};
}
}
// Per-route versioning - different versions for different routes
@Controller('users')
export class UsersController {
@Get()
@Version('1')
findAllV1(): Promise<UserV1Response[]> {
return this.usersService.findAllV1();
}
@Get()
@Version('2')
findAllV2(): Promise<UserV2Response[]> {
return this.usersService.findAllV2();
}
@Get(':id')
@Version(['1', '2']) // Same handler for multiple versions
findOne(@Param('id') id: string): Promise<User> {
return this.usersService.findOne(id);
}
@Post()
@Version(VERSION_NEUTRAL) // Available in all versions
create(@Body() dto: CreateUserDto): Promise<User> {
return this.usersService.create(dto);
}
}
// Shared service with version-specific logic
@Injectable()
export class UsersService {
async findOne(id: string, version: string): Promise<any> {
const user = await this.repo.findOne({ where: { id } });
if (version === '1') {
return this.toV1Response(user);
}
return this.toV2Response(user);
}
private toV1Response(user: User): UserV1Response {
return {
id: user.id,
name: `${user.firstName} ${user.lastName}`,
email: user.email,
};
}
private toV2Response(user: User): UserV2Response {
return {
id: user.id,
firstName: user.firstName,
lastName: user.lastName,
emailAddress: user.email,
createdAt: user.createdAt,
};
}
}
// Controller extracts version
@Controller('users')
export class UsersController {
@Get(':id')
async findOne(
@Param('id') id: string,
@Headers('X-API-Version') version: string = '1',
): Promise<any> {
return this.usersService.findOne(id, version);
}
}
// Deprecation strategy - mark old versions as deprecated
@Controller('users')
@Version('1')
@UseInterceptors(DeprecationInterceptor)
export class UsersV1Controller {
// All V1 routes will include deprecation warning
}
@Injectable()
export class DeprecationInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const response = context.switchToHttp().getResponse();
response.setHeader('Deprecation', 'true');
response.setHeader('Sunset', 'Sat, 1 Jan 2025 00:00:00 GMT');
response.setHeader('Link', '</v2/users>; rel="successor-version"');
return next.handle();
}
}
```
Reference: [NestJS Versioning](https://docs.nestjs.com/techniques/versioning)
@@ -0,0 +1,80 @@
---
title: Avoid Circular Dependencies
impact: CRITICAL
impactDescription: "#1 cause of runtime crashes"
tags: architecture, modules, dependencies
---
## Avoid Circular Dependencies
Circular dependencies occur when Module A imports Module B, and Module B imports Module A (directly or transitively). NestJS can sometimes resolve these through forward references, but they indicate architectural problems and should be avoided. This is the #1 cause of runtime crashes in NestJS applications.
**Incorrect (circular module imports):**
```typescript
// users.module.ts
@Module({
imports: [OrdersModule], // Orders needs Users, Users needs Orders = circular
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
// orders.module.ts
@Module({
imports: [UsersModule], // Circular dependency!
providers: [OrdersService],
exports: [OrdersService],
})
export class OrdersModule {}
```
**Correct (extract shared logic or use events):**
```typescript
// Option 1: Extract shared logic to a third module
// shared.module.ts
@Module({
providers: [SharedService],
exports: [SharedService],
})
export class SharedModule {}
// users.module.ts
@Module({
imports: [SharedModule],
providers: [UsersService],
})
export class UsersModule {}
// orders.module.ts
@Module({
imports: [SharedModule],
providers: [OrdersService],
})
export class OrdersModule {}
// Option 2: Use events for decoupled communication
// users.service.ts
@Injectable()
export class UsersService {
constructor(private eventEmitter: EventEmitter2) {}
async createUser(data: CreateUserDto) {
const user = await this.userRepo.save(data);
this.eventEmitter.emit('user.created', user);
return user;
}
}
// orders.service.ts
@Injectable()
export class OrdersService {
@OnEvent('user.created')
handleUserCreated(user: User) {
// React to user creation without direct dependency
}
}
```
Reference: [NestJS Circular Dependency](https://docs.nestjs.com/fundamentals/circular-dependency)
@@ -0,0 +1,82 @@
---
title: Organize by Feature Modules
impact: CRITICAL
impactDescription: "3-5x faster onboarding and development"
tags: architecture, modules, organization
---
## Organize by Feature Modules
Organize your application into feature modules that encapsulate related functionality. Each feature module should be self-contained with its own controllers, services, entities, and DTOs. Avoid organizing by technical layer (all controllers together, all services together). This enables 3-5x faster onboarding and feature development.
**Incorrect (technical layer organization):**
```typescript
// Technical layer organization (anti-pattern)
src/
controllers/
users.controller.ts
orders.controller.ts
products.controller.ts
services/
users.service.ts
orders.service.ts
products.service.ts
entities/
user.entity.ts
order.entity.ts
product.entity.ts
app.module.ts // Imports everything directly
```
**Correct (feature module organization):**
```typescript
// Feature module organization
src/
users/
dto/
create-user.dto.ts
update-user.dto.ts
entities/
user.entity.ts
users.controller.ts
users.service.ts
users.repository.ts
users.module.ts
orders/
dto/
entities/
orders.controller.ts
orders.service.ts
orders.module.ts
shared/
guards/
interceptors/
filters/
shared.module.ts
app.module.ts
// users.module.ts
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService, UsersRepository],
exports: [UsersService], // Only export what others need
})
export class UsersModule {}
// app.module.ts
@Module({
imports: [
ConfigModule.forRoot(),
TypeOrmModule.forRoot(),
UsersModule,
OrdersModule,
SharedModule,
],
})
export class AppModule {}
```
Reference: [NestJS Modules](https://docs.nestjs.com/modules)
@@ -0,0 +1,141 @@
---
title: Use Proper Module Sharing Patterns
impact: CRITICAL
impactDescription: Prevents duplicate instances, memory leaks, and state inconsistency
tags: architecture, modules, sharing, exports
---
## Use Proper Module Sharing Patterns
NestJS modules are singletons by default. When a service is properly exported from a module and that module is imported elsewhere, the same instance is shared. However, providing a service in multiple modules creates separate instances, leading to memory waste, state inconsistency, and confusing behavior. Always encapsulate services in dedicated modules, export them explicitly, and import the module where needed.
**Incorrect (service provided in multiple modules):**
```typescript
// StorageService provided directly in multiple modules - WRONG
// storage.service.ts
@Injectable()
export class StorageService {
private cache = new Map(); // Each instance has separate state!
store(key: string, value: any) {
this.cache.set(key, value);
}
}
// app.module.ts
@Module({
providers: [StorageService], // Instance #1
controllers: [AppController],
})
export class AppModule {}
// videos.module.ts
@Module({
providers: [StorageService], // Instance #2 - different from AppModule!
controllers: [VideosController],
})
export class VideosModule {}
// Problems:
// 1. Two separate StorageService instances exist
// 2. cache.set() in VideosModule doesn't affect AppModule's cache
// 3. Memory wasted on duplicate instances
// 4. Debugging nightmares when state doesn't sync
```
**Correct (dedicated module with exports):**
```typescript
// storage/storage.module.ts
@Module({
providers: [StorageService],
exports: [StorageService], // Make available to importers
})
export class StorageModule {}
// videos/videos.module.ts
@Module({
imports: [StorageModule], // Import the module, not the service
controllers: [VideosController],
providers: [VideosService],
})
export class VideosModule {}
// channels/channels.module.ts
@Module({
imports: [StorageModule], // Same instance shared
controllers: [ChannelsController],
providers: [ChannelsService],
})
export class ChannelsModule {}
// app.module.ts
@Module({
imports: [
StorageModule, // Only if AppModule itself needs StorageService
VideosModule,
ChannelsModule,
],
})
export class AppModule {}
// Now all modules share the SAME StorageService instance
```
**When to use @Global() (sparingly):**
```typescript
// ONLY for truly cross-cutting concerns
@Global()
@Module({
providers: [ConfigService, LoggerService],
exports: [ConfigService, LoggerService],
})
export class CoreModule {}
// Import once in AppModule
@Module({
imports: [CoreModule], // Registered globally, available everywhere
})
export class AppModule {}
// Other modules don't need to import CoreModule
@Module({
controllers: [UsersController],
providers: [UsersService], // Can inject ConfigService without importing
})
export class UsersModule {}
// WARNING: Don't make everything global!
// - Hides dependencies (can't see what a module needs from imports)
// - Makes testing harder
// - Reserve for: config, logging, database connections
```
**Module re-exporting pattern:**
```typescript
// common.module.ts - shared utilities
@Module({
providers: [DateService, ValidationService],
exports: [DateService, ValidationService],
})
export class CommonModule {}
// core.module.ts - re-exports common for convenience
@Module({
imports: [CommonModule, DatabaseModule],
exports: [CommonModule, DatabaseModule], // Re-export for consumers
})
export class CoreModule {}
// feature.module.ts - imports CoreModule, gets both
@Module({
imports: [CoreModule], // Gets CommonModule + DatabaseModule
controllers: [FeatureController],
})
export class FeatureModule {}
```
Reference: [NestJS Modules](https://docs.nestjs.com/modules#shared-modules)
@@ -0,0 +1,106 @@
---
title: Single Responsibility for Services
impact: CRITICAL
impactDescription: "40%+ improvement in testability"
tags: architecture, services, single-responsibility
---
## Single Responsibility for Services
Each service should have a single, well-defined responsibility. Avoid "god services" that handle multiple unrelated concerns. If a service name includes "And" or handles more than one domain concept, it likely violates single responsibility. This reduces complexity and improves testability by 40%+.
**Incorrect (god service anti-pattern):**
```typescript
// God service anti-pattern
@Injectable()
export class UserAndOrderService {
constructor(
private userRepo: UserRepository,
private orderRepo: OrderRepository,
private mailer: MailService,
private payment: PaymentService,
) {}
async createUser(dto: CreateUserDto) {
const user = await this.userRepo.save(dto);
await this.mailer.sendWelcome(user);
return user;
}
async createOrder(userId: string, dto: CreateOrderDto) {
const order = await this.orderRepo.save({ userId, ...dto });
await this.payment.charge(order);
await this.mailer.sendOrderConfirmation(order);
return order;
}
async calculateOrderStats(userId: string) {
// Stats logic mixed in
}
async validatePayment(orderId: string) {
// Payment logic mixed in
}
}
```
**Correct (focused services with single responsibility):**
```typescript
// Focused services with single responsibility
@Injectable()
export class UsersService {
constructor(private userRepo: UserRepository) {}
async create(dto: CreateUserDto): Promise<User> {
return this.userRepo.save(dto);
}
async findById(id: string): Promise<User> {
return this.userRepo.findOneOrFail({ where: { id } });
}
}
@Injectable()
export class OrdersService {
constructor(private orderRepo: OrderRepository) {}
async create(userId: string, dto: CreateOrderDto): Promise<Order> {
return this.orderRepo.save({ userId, ...dto });
}
async findByUser(userId: string): Promise<Order[]> {
return this.orderRepo.find({ where: { userId } });
}
}
@Injectable()
export class OrderStatsService {
constructor(private orderRepo: OrderRepository) {}
async calculateForUser(userId: string): Promise<OrderStats> {
// Focused stats calculation
}
}
// Orchestration in controller or dedicated orchestrator
@Controller('orders')
export class OrdersController {
constructor(
private orders: OrdersService,
private payment: PaymentService,
private notifications: NotificationService,
) {}
@Post()
async create(@CurrentUser() user: User, @Body() dto: CreateOrderDto) {
const order = await this.orders.create(user.id, dto);
await this.payment.charge(order);
await this.notifications.sendOrderConfirmation(order);
return order;
}
}
```
Reference: [NestJS Providers](https://docs.nestjs.com/providers)
@@ -0,0 +1,108 @@
---
title: Use Event-Driven Architecture for Decoupling
impact: MEDIUM-HIGH
impactDescription: Enables async processing and modularity
tags: architecture, events, decoupling
---
## Use Event-Driven Architecture for Decoupling
Use `@nestjs/event-emitter` for intra-service events and message brokers for inter-service communication. Events allow modules to react to changes without direct dependencies, improving modularity and enabling async processing.
**Incorrect (direct service coupling):**
```typescript
// Direct service coupling
@Injectable()
export class OrdersService {
constructor(
private inventoryService: InventoryService,
private emailService: EmailService,
private analyticsService: AnalyticsService,
private notificationService: NotificationService,
private loyaltyService: LoyaltyService,
) {}
async createOrder(dto: CreateOrderDto): Promise<Order> {
const order = await this.repo.save(dto);
// Tight coupling - OrdersService knows about all consumers
await this.inventoryService.reserve(order.items);
await this.emailService.sendConfirmation(order);
await this.analyticsService.track('order_created', order);
await this.notificationService.push(order.userId, 'Order placed');
await this.loyaltyService.addPoints(order.userId, order.total);
// Adding new behavior requires modifying this service
return order;
}
}
```
**Correct (event-driven decoupling):**
```typescript
// Use EventEmitter for decoupling
import { EventEmitter2 } from '@nestjs/event-emitter';
// Define event
export class OrderCreatedEvent {
constructor(
public readonly orderId: string,
public readonly userId: string,
public readonly items: OrderItem[],
public readonly total: number,
) {}
}
// Service emits events
@Injectable()
export class OrdersService {
constructor(
private eventEmitter: EventEmitter2,
private repo: Repository<Order>,
) {}
async createOrder(dto: CreateOrderDto): Promise<Order> {
const order = await this.repo.save(dto);
// Emit event - no knowledge of consumers
this.eventEmitter.emit(
'order.created',
new OrderCreatedEvent(order.id, order.userId, order.items, order.total),
);
return order;
}
}
// Listeners in separate modules
@Injectable()
export class InventoryListener {
@OnEvent('order.created')
async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
await this.inventoryService.reserve(event.items);
}
}
@Injectable()
export class EmailListener {
@OnEvent('order.created')
async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
await this.emailService.sendConfirmation(event.orderId);
}
}
@Injectable()
export class AnalyticsListener {
@OnEvent('order.created')
async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
await this.analyticsService.track('order_created', {
orderId: event.orderId,
total: event.total,
});
}
}
```
Reference: [NestJS Events](https://docs.nestjs.com/techniques/events)
@@ -0,0 +1,97 @@
---
title: Use Repository Pattern for Data Access
impact: HIGH
impactDescription: Decouples business logic from database
tags: architecture, repository, data-access
---
## Use Repository Pattern for Data Access
Create custom repositories to encapsulate complex queries and database logic. This keeps services focused on business logic, makes testing easier with mock repositories, and allows changing database implementations without affecting business code.
**Incorrect (complex queries in services):**
```typescript
// Complex queries in services
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User) private repo: Repository<User>,
) {}
async findActiveWithOrders(minOrders: number): Promise<User[]> {
// Complex query logic mixed with business logic
return this.repo
.createQueryBuilder('user')
.leftJoinAndSelect('user.orders', 'order')
.where('user.isActive = :active', { active: true })
.andWhere('user.deletedAt IS NULL')
.groupBy('user.id')
.having('COUNT(order.id) >= :min', { min: minOrders })
.orderBy('user.createdAt', 'DESC')
.getMany();
}
// Service becomes bloated with query logic
}
```
**Correct (custom repository with encapsulated queries):**
```typescript
// Custom repository with encapsulated queries
@Injectable()
export class UsersRepository {
constructor(
@InjectRepository(User) private repo: Repository<User>,
) {}
async findById(id: string): Promise<User | null> {
return this.repo.findOne({ where: { id } });
}
async findByEmail(email: string): Promise<User | null> {
return this.repo.findOne({ where: { email } });
}
async findActiveWithMinOrders(minOrders: number): Promise<User[]> {
return this.repo
.createQueryBuilder('user')
.leftJoinAndSelect('user.orders', 'order')
.where('user.isActive = :active', { active: true })
.andWhere('user.deletedAt IS NULL')
.groupBy('user.id')
.having('COUNT(order.id) >= :min', { min: minOrders })
.orderBy('user.createdAt', 'DESC')
.getMany();
}
async save(user: User): Promise<User> {
return this.repo.save(user);
}
}
// Clean service with business logic only
@Injectable()
export class UsersService {
constructor(private usersRepo: UsersRepository) {}
async getActiveUsersWithOrders(): Promise<User[]> {
return this.usersRepo.findActiveWithMinOrders(1);
}
async create(dto: CreateUserDto): Promise<User> {
const existing = await this.usersRepo.findByEmail(dto.email);
if (existing) {
throw new ConflictException('Email already registered');
}
const user = new User();
user.email = dto.email;
user.name = dto.name;
return this.usersRepo.save(user);
}
}
```
Reference: [Repository Pattern](https://martinfowler.com/eaaCatalog/repository.html)
@@ -0,0 +1,139 @@
---
title: Avoid N+1 Query Problems
impact: HIGH
impactDescription: N+1 queries are one of the most common performance killers
tags: database, n-plus-one, queries, performance
---
## Avoid N+1 Query Problems
N+1 queries occur when you fetch a list of entities, then make an additional query for each entity to load related data. Use eager loading with `relations`, query builder joins, or DataLoader to batch queries efficiently.
**Incorrect (lazy loading in loops causes N+1):**
```typescript
// Lazy loading in loops causes N+1
@Injectable()
export class OrdersService {
async getOrdersWithItems(userId: string): Promise<Order[]> {
const orders = await this.orderRepo.find({ where: { userId } });
// 1 query for orders
for (const order of orders) {
// N additional queries - one per order!
order.items = await this.itemRepo.find({ where: { orderId: order.id } });
}
return orders;
}
}
// Accessing lazy relations without loading
@Controller('users')
export class UsersController {
@Get()
async findAll(): Promise<User[]> {
const users = await this.userRepo.find();
// If User.posts is lazy-loaded, serializing triggers N queries
return users; // Each user.posts access = 1 query
}
}
```
**Correct (use relations for eager loading):**
```typescript
// Use relations option for eager loading
@Injectable()
export class OrdersService {
async getOrdersWithItems(userId: string): Promise<Order[]> {
// Single query with JOIN
return this.orderRepo.find({
where: { userId },
relations: ['items', 'items.product'],
});
}
}
// Use QueryBuilder for complex joins
@Injectable()
export class UsersService {
async getUsersWithPostCounts(): Promise<UserWithPostCount[]> {
return this.userRepo
.createQueryBuilder('user')
.leftJoin('user.posts', 'post')
.select('user.id', 'id')
.addSelect('user.name', 'name')
.addSelect('COUNT(post.id)', 'postCount')
.groupBy('user.id')
.getRawMany();
}
async getActiveUsersWithPosts(): Promise<User[]> {
return this.userRepo
.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'post')
.leftJoinAndSelect('post.comments', 'comment')
.where('user.isActive = :active', { active: true })
.andWhere('post.status = :status', { status: 'published' })
.getMany();
}
}
// Use find options for specific fields
async getOrderSummaries(userId: string): Promise<OrderSummary[]> {
return this.orderRepo.find({
where: { userId },
relations: ['items'],
select: {
id: true,
total: true,
status: true,
items: {
id: true,
quantity: true,
price: true,
},
},
});
}
// Use DataLoader for GraphQL to batch and cache queries
import DataLoader from 'dataloader';
@Injectable({ scope: Scope.REQUEST })
export class PostsLoader {
constructor(private postsService: PostsService) {}
readonly batchPosts = new DataLoader<string, Post[]>(async (userIds) => {
// Single query for all users' posts
const posts = await this.postsService.findByUserIds([...userIds]);
// Group by userId
const postsMap = new Map<string, Post[]>();
for (const post of posts) {
const userPosts = postsMap.get(post.userId) || [];
userPosts.push(post);
postsMap.set(post.userId, userPosts);
}
// Return in same order as input
return userIds.map((id) => postsMap.get(id) || []);
});
}
// In resolver
@ResolveField()
async posts(@Parent() user: User): Promise<Post[]> {
// DataLoader batches multiple calls into single query
return this.postsLoader.batchPosts.load(user.id);
}
// Enable query logging in development to detect N+1
TypeOrmModule.forRoot({
logging: ['query', 'error'],
logger: 'advanced-console',
});
```
Reference: [TypeORM Relations](https://typeorm.io/relations)
@@ -0,0 +1,61 @@
---
title: Use Database Migrations
impact: HIGH
impactDescription: Enables safe, repeatable database schema changes
tags: database, migrations, drizzle, schema
---
## Use Database Migrations
Never push schema changes with `drizzle-kit push` in production. Use versioned SQL migrations for all schema changes. Migrations provide version control for your database, enable safe rollbacks, and ensure consistency across all environments.
**Incorrect (pushing schema or running ad-hoc SQL):**
```typescript
// drizzle-kit push against production
// Mutates the live database with no rollback file
@Injectable()
export class DatabaseService {
async addColumn(): Promise<void> {
await this.db.execute(sql`ALTER TABLE users ADD COLUMN age INT`);
}
}
```
**Correct (generate and apply Drizzle migrations):**
```typescript
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './drizzle/schema.ts',
out: './drizzle/migrations',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
```
```bash
# Generate a migration from schema diffs
npx drizzle-kit generate
# Apply migrations
npx drizzle-kit migrate
```
```typescript
// drizzle/schema.ts
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: varchar('email', { length: 255 }).notNull(),
age: integer('age').default(0),
});
```
Always keep `down`/rollback SQL when a migration is destructive. Never edit an already-applied migration file; add a new one.
Reference: [Drizzle Kit Migrations](https://orm.drizzle.team/docs/kit-overview)
@@ -0,0 +1,140 @@
---
title: Use Transactions for Multi-Step Operations
impact: HIGH
impactDescription: Ensures data consistency in multi-step operations
tags: database, transactions, typeorm, consistency
---
## Use Transactions for Multi-Step Operations
When multiple database operations must succeed or fail together, wrap them in a transaction. This prevents partial updates that leave your data in an inconsistent state. Use TypeORM's transaction APIs or the DataSource query runner for complex scenarios.
**Incorrect (multiple saves without transaction):**
```typescript
// Multiple saves without transaction
@Injectable()
export class OrdersService {
async createOrder(userId: string, items: OrderItem[]): Promise<Order> {
// If any step fails, data is inconsistent
const order = await this.orderRepo.save({ userId, status: 'pending' });
for (const item of items) {
await this.orderItemRepo.save({ orderId: order.id, ...item });
await this.inventoryRepo.decrement({ productId: item.productId }, 'stock', item.quantity);
}
await this.paymentService.charge(order.id);
// If payment fails, order and inventory are already modified!
return order;
}
}
```
**Correct (use DataSource.transaction for automatic rollback):**
```typescript
// Use DataSource.transaction() for automatic rollback
@Injectable()
export class OrdersService {
constructor(private dataSource: DataSource) {}
async createOrder(userId: string, items: OrderItem[]): Promise<Order> {
return this.dataSource.transaction(async (manager) => {
// All operations use the same transactional manager
const order = await manager.save(Order, { userId, status: 'pending' });
for (const item of items) {
await manager.save(OrderItem, { orderId: order.id, ...item });
await manager.decrement(
Inventory,
{ productId: item.productId },
'stock',
item.quantity,
);
}
// If this throws, everything rolls back
await this.paymentService.chargeWithManager(manager, order.id);
return order;
});
}
}
// QueryRunner for manual transaction control
@Injectable()
export class TransferService {
constructor(private dataSource: DataSource) {}
async transfer(fromId: string, toId: string, amount: number): Promise<void> {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// Debit source account
await queryRunner.manager.decrement(
Account,
{ id: fromId },
'balance',
amount,
);
// Verify sufficient funds
const source = await queryRunner.manager.findOne(Account, {
where: { id: fromId },
});
if (source.balance < 0) {
throw new BadRequestException('Insufficient funds');
}
// Credit destination account
await queryRunner.manager.increment(
Account,
{ id: toId },
'balance',
amount,
);
// Log the transaction
await queryRunner.manager.save(TransactionLog, {
fromId,
toId,
amount,
timestamp: new Date(),
});
await queryRunner.commitTransaction();
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
}
}
// Repository method with transaction support
@Injectable()
export class UsersRepository {
constructor(
@InjectRepository(User) private repo: Repository<User>,
private dataSource: DataSource,
) {}
async createWithProfile(
userData: CreateUserDto,
profileData: CreateProfileDto,
): Promise<User> {
return this.dataSource.transaction(async (manager) => {
const user = await manager.save(User, userData);
await manager.save(Profile, { ...profileData, userId: user.id });
return user;
});
}
}
```
Reference: [TypeORM Transactions](https://typeorm.io/transactions)
@@ -0,0 +1,222 @@
---
title: Implement Graceful Shutdown
impact: MEDIUM-HIGH
impactDescription: Proper shutdown handling ensures zero-downtime deployments
tags: devops, graceful-shutdown, lifecycle, kubernetes
---
## Implement Graceful Shutdown
Handle SIGTERM and SIGINT signals to gracefully shutdown your NestJS application. Stop accepting new requests, wait for in-flight requests to complete, close database connections, and clean up resources. This prevents data loss and connection errors during deployments.
**Incorrect (ignoring shutdown signals):**
```typescript
// Ignore shutdown signals
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
// App crashes immediately on SIGTERM
// In-flight requests fail
// Database connections are abruptly closed
}
// Long-running tasks without cancellation
@Injectable()
export class ProcessingService {
async processLargeFile(file: File): Promise<void> {
// No way to interrupt this during shutdown
for (let i = 0; i < file.chunks.length; i++) {
await this.processChunk(file.chunks[i]);
// May run for minutes, blocking shutdown
}
}
}
```
**Correct (enable shutdown hooks and handle cleanup):**
```typescript
// Enable shutdown hooks in main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Enable shutdown hooks
app.enableShutdownHooks();
// Optional: Add timeout for forced shutdown
const server = await app.listen(3000);
server.setTimeout(30000); // 30 second timeout
// Handle graceful shutdown
const signals = ['SIGTERM', 'SIGINT'];
signals.forEach((signal) => {
process.on(signal, async () => {
console.log(`Received ${signal}, starting graceful shutdown...`);
// Stop accepting new connections
server.close(async () => {
console.log('HTTP server closed');
await app.close();
process.exit(0);
});
// Force exit after timeout
setTimeout(() => {
console.error('Forced shutdown after timeout');
process.exit(1);
}, 30000);
});
});
}
// Lifecycle hooks for cleanup
@Injectable()
export class DatabaseService implements OnApplicationShutdown {
private readonly connections: Connection[] = [];
async onApplicationShutdown(signal?: string): Promise<void> {
console.log(`Database service shutting down on ${signal}`);
// Close all connections gracefully
await Promise.all(
this.connections.map((conn) => conn.close()),
);
console.log('All database connections closed');
}
}
// Queue processor with graceful shutdown
@Injectable()
export class QueueService implements OnApplicationShutdown, OnModuleDestroy {
private isShuttingDown = false;
onModuleDestroy(): void {
this.isShuttingDown = true;
}
async onApplicationShutdown(): Promise<void> {
// Wait for current jobs to complete
await this.queue.close();
}
async processJob(job: Job): Promise<void> {
if (this.isShuttingDown) {
throw new Error('Service is shutting down');
}
await this.doWork(job);
}
}
// WebSocket gateway cleanup
@WebSocketGateway()
export class EventsGateway implements OnApplicationShutdown {
@WebSocketServer()
server: Server;
async onApplicationShutdown(): Promise<void> {
// Notify all connected clients
this.server.emit('shutdown', { message: 'Server is shutting down' });
// Close all connections
this.server.disconnectSockets();
}
}
// Health check integration
@Injectable()
export class ShutdownService {
private isShuttingDown = false;
startShutdown(): void {
this.isShuttingDown = true;
}
isShutdown(): boolean {
return this.isShuttingDown;
}
}
@Controller('health')
export class HealthController {
constructor(private shutdownService: ShutdownService) {}
@Get('ready')
@HealthCheck()
readiness(): Promise<HealthCheckResult> {
// Return 503 during shutdown - k8s stops sending traffic
if (this.shutdownService.isShutdown()) {
throw new ServiceUnavailableException('Shutting down');
}
return this.health.check([
() => this.db.pingCheck('database'),
]);
}
}
// Integrate with shutdown
@Injectable()
export class AppShutdownService implements OnApplicationShutdown {
constructor(private shutdownService: ShutdownService) {}
async onApplicationShutdown(): Promise<void> {
// Mark as unhealthy first
this.shutdownService.startShutdown();
// Wait for k8s to update endpoints
await this.sleep(5000);
// Then proceed with cleanup
}
}
// Request tracking for in-flight requests
@Injectable()
export class RequestTracker implements NestMiddleware, OnApplicationShutdown {
private activeRequests = 0;
private isShuttingDown = false;
private shutdownPromise: Promise<void> | null = null;
private resolveShutdown: (() => void) | null = null;
use(req: Request, res: Response, next: NextFunction): void {
if (this.isShuttingDown) {
res.status(503).send('Service Unavailable');
return;
}
this.activeRequests++;
res.on('finish', () => {
this.activeRequests--;
if (this.isShuttingDown && this.activeRequests === 0 && this.resolveShutdown) {
this.resolveShutdown();
}
});
next();
}
async onApplicationShutdown(): Promise<void> {
this.isShuttingDown = true;
if (this.activeRequests > 0) {
console.log(`Waiting for ${this.activeRequests} requests to complete`);
this.shutdownPromise = new Promise((resolve) => {
this.resolveShutdown = resolve;
});
// Wait with timeout
await Promise.race([
this.shutdownPromise,
new Promise((resolve) => setTimeout(resolve, 30000)),
]);
}
console.log('All requests completed');
}
}
```
Reference: [NestJS Lifecycle Events](https://docs.nestjs.com/fundamentals/lifecycle-events)
@@ -0,0 +1,167 @@
---
title: Use ConfigModule for Environment Configuration
impact: LOW-MEDIUM
impactDescription: Proper configuration prevents deployment failures
tags: devops, configuration, environment, validation
---
## Use ConfigModule for Environment Configuration
Use `@nestjs/config` for environment-based configuration. Validate configuration at startup to fail fast on misconfigurations. Use namespaced configuration for organization and type safety.
**Incorrect (accessing process.env directly):**
```typescript
// Access process.env directly
@Injectable()
export class DatabaseService {
constructor() {
// No validation, can fail at runtime
this.connection = new Pool({
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT), // NaN if missing
password: process.env.DB_PASSWORD, // undefined if missing
});
}
}
// Scattered env access
@Injectable()
export class EmailService {
sendEmail() {
// Different services access env differently
const apiKey = process.env.SENDGRID_API_KEY || 'default';
// Typos go unnoticed: process.env.SENDGRID_API_KY
}
}
```
**Correct (use @nestjs/config with validation):**
```typescript
// Setup validated configuration
import { ConfigModule, ConfigService, registerAs } from '@nestjs/config';
import * as Joi from 'joi';
// config/database.config.ts
export const databaseConfig = registerAs('database', () => ({
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT, 10),
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
}));
// config/app.config.ts
export const appConfig = registerAs('app', () => ({
port: parseInt(process.env.PORT, 10) || 3000,
environment: process.env.NODE_ENV || 'development',
apiPrefix: process.env.API_PREFIX || 'api',
}));
// config/validation.schema.ts
export const validationSchema = Joi.object({
NODE_ENV: Joi.string()
.valid('development', 'production', 'test')
.default('development'),
PORT: Joi.number().default(3000),
DB_HOST: Joi.string().required(),
DB_PORT: Joi.number().default(5432),
DB_USERNAME: Joi.string().required(),
DB_PASSWORD: Joi.string().required(),
DB_NAME: Joi.string().required(),
JWT_SECRET: Joi.string().min(32).required(),
REDIS_URL: Joi.string().uri().required(),
});
// app.module.ts
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // Available everywhere without importing
load: [databaseConfig, appConfig],
validationSchema,
validationOptions: {
abortEarly: true, // Stop on first error
allowUnknown: true, // Allow other env vars
},
}),
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
type: 'postgres',
host: config.get('database.host'),
port: config.get('database.port'),
username: config.get('database.username'),
password: config.get('database.password'),
database: config.get('database.database'),
autoLoadEntities: true,
}),
}),
],
})
export class AppModule {}
// Type-safe configuration access
export interface AppConfig {
port: number;
environment: 'development' | 'production' | 'test';
apiPrefix: string;
}
export interface DatabaseConfig {
host: string;
port: number;
username: string;
password: string;
database: string;
}
// Type-safe access
@Injectable()
export class AppService {
constructor(private config: ConfigService) {}
getPort(): number {
// Type-safe with generic
return this.config.get<number>('app.port');
}
getDatabaseConfig(): DatabaseConfig {
return this.config.get<DatabaseConfig>('database');
}
}
// Inject namespaced config directly
@Injectable()
export class DatabaseService {
constructor(
@Inject(databaseConfig.KEY)
private dbConfig: ConfigType<typeof databaseConfig>,
) {
// Full type inference!
const host = this.dbConfig.host; // string
const port = this.dbConfig.port; // number
}
}
// Environment files support
ConfigModule.forRoot({
envFilePath: [
`.env.${process.env.NODE_ENV}.local`,
`.env.${process.env.NODE_ENV}`,
'.env.local',
'.env',
],
});
// .env.development
// DB_HOST=localhost
// DB_PORT=5432
// .env.production
// DB_HOST=prod-db.example.com
// DB_PORT=5432
```
Reference: [NestJS Configuration](https://docs.nestjs.com/techniques/configuration)
@@ -0,0 +1,232 @@
---
title: Use Structured Logging
impact: MEDIUM-HIGH
impactDescription: Structured logging enables effective debugging and monitoring
tags: devops, logging, structured-logs, pino
---
## Use Structured Logging
Use NestJS Logger with structured JSON output in production. Include contextual information (request ID, user ID, operation) to trace requests across services. Avoid console.log and implement proper log levels.
**Incorrect (using console.log in production):**
```typescript
// Use console.log in production
@Injectable()
export class UsersService {
async createUser(dto: CreateUserDto): Promise<User> {
console.log('Creating user:', dto);
// Not structured, no levels, lost in production logs
try {
const user = await this.repo.save(dto);
console.log('User created:', user.id);
return user;
} catch (error) {
console.log('Error:', error); // Using log for errors
throw error;
}
}
}
// Log sensitive data
console.log('Login attempt:', { email, password }); // SECURITY RISK!
// Inconsistent log format
logger.log('User ' + userId + ' created at ' + new Date());
// Hard to parse, no structure
```
**Correct (use structured logging with context):**
```typescript
// Configure logger in main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger:
process.env.NODE_ENV === 'production'
? ['error', 'warn', 'log']
: ['error', 'warn', 'log', 'debug', 'verbose'],
});
}
// Use NestJS Logger with context
@Injectable()
export class UsersService {
private readonly logger = new Logger(UsersService.name);
async createUser(dto: CreateUserDto): Promise<User> {
this.logger.log('Creating user', { email: dto.email });
try {
const user = await this.repo.save(dto);
this.logger.log('User created', { userId: user.id });
return user;
} catch (error) {
this.logger.error('Failed to create user', error.stack, {
email: dto.email,
});
throw error;
}
}
}
// Custom logger for JSON output
@Injectable()
export class JsonLogger implements LoggerService {
log(message: string, context?: object): void {
console.log(
JSON.stringify({
level: 'info',
timestamp: new Date().toISOString(),
message,
...context,
}),
);
}
error(message: string, trace?: string, context?: object): void {
console.error(
JSON.stringify({
level: 'error',
timestamp: new Date().toISOString(),
message,
trace,
...context,
}),
);
}
warn(message: string, context?: object): void {
console.warn(
JSON.stringify({
level: 'warn',
timestamp: new Date().toISOString(),
message,
...context,
}),
);
}
debug(message: string, context?: object): void {
console.debug(
JSON.stringify({
level: 'debug',
timestamp: new Date().toISOString(),
message,
...context,
}),
);
}
}
// Request context logging with ClsModule
import { ClsModule, ClsService } from 'nestjs-cls';
@Module({
imports: [
ClsModule.forRoot({
global: true,
middleware: {
mount: true,
generateId: true,
},
}),
],
})
export class AppModule {}
// Middleware to set request context
@Injectable()
export class RequestContextMiddleware implements NestMiddleware {
constructor(private cls: ClsService) {}
use(req: Request, res: Response, next: NextFunction): void {
const requestId = req.headers['x-request-id'] || randomUUID();
this.cls.set('requestId', requestId);
this.cls.set('userId', req.user?.id);
res.setHeader('x-request-id', requestId);
next();
}
}
// Logger that includes request context
@Injectable()
export class ContextLogger {
constructor(private cls: ClsService) {}
log(message: string, data?: object): void {
console.log(
JSON.stringify({
level: 'info',
timestamp: new Date().toISOString(),
requestId: this.cls.get('requestId'),
userId: this.cls.get('userId'),
message,
...data,
}),
);
}
error(message: string, error: Error, data?: object): void {
console.error(
JSON.stringify({
level: 'error',
timestamp: new Date().toISOString(),
requestId: this.cls.get('requestId'),
userId: this.cls.get('userId'),
message,
error: error.message,
stack: error.stack,
...data,
}),
);
}
}
// Pino integration for high-performance logging
import { LoggerModule } from 'nestjs-pino';
@Module({
imports: [
LoggerModule.forRoot({
pinoHttp: {
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
transport:
process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty' }
: undefined,
redact: ['req.headers.authorization', 'req.body.password'],
serializers: {
req: (req) => ({
method: req.method,
url: req.url,
query: req.query,
}),
res: (res) => ({
statusCode: res.statusCode,
}),
},
},
}),
],
})
export class AppModule {}
// Usage with Pino
@Injectable()
export class UsersService {
constructor(private logger: PinoLogger) {
this.logger.setContext(UsersService.name);
}
async findOne(id: string): Promise<User> {
this.logger.info({ userId: id }, 'Finding user');
// Pino uses first arg for data, second for message
}
}
```
Reference: [NestJS Logger](https://docs.nestjs.com/techniques/logger)
@@ -0,0 +1,104 @@
---
title: Avoid Service Locator Anti-Pattern
impact: HIGH
impactDescription: Hides dependencies and breaks testability
tags: dependency-injection, anti-patterns, testing
---
## Avoid Service Locator Anti-Pattern
Avoid using `ModuleRef.get()` or global containers to resolve dependencies at runtime. This hides dependencies, makes code harder to test, and breaks the benefits of dependency injection. Use constructor injection instead.
**Incorrect (service locator anti-pattern):**
```typescript
// Use ModuleRef to get dependencies dynamically
@Injectable()
export class OrdersService {
constructor(private moduleRef: ModuleRef) {}
async createOrder(dto: CreateOrderDto): Promise<Order> {
// Dependencies are hidden - not visible in constructor
const usersService = this.moduleRef.get(UsersService);
const inventoryService = this.moduleRef.get(InventoryService);
const paymentService = this.moduleRef.get(PaymentService);
const user = await usersService.findOne(dto.userId);
// ... rest of logic
}
}
// Global singleton container
class ServiceContainer {
private static instance: ServiceContainer;
private services = new Map<string, any>();
static getInstance(): ServiceContainer {
if (!this.instance) {
this.instance = new ServiceContainer();
}
return this.instance;
}
get<T>(key: string): T {
return this.services.get(key);
}
}
```
**Correct (constructor injection with explicit dependencies):**
```typescript
// Use constructor injection - dependencies are explicit
@Injectable()
export class OrdersService {
constructor(
private usersService: UsersService,
private inventoryService: InventoryService,
private paymentService: PaymentService,
) {}
async createOrder(dto: CreateOrderDto): Promise<Order> {
const user = await this.usersService.findOne(dto.userId);
const inventory = await this.inventoryService.check(dto.items);
// Dependencies are clear and testable
}
}
// Easy to test with mocks
describe('OrdersService', () => {
let service: OrdersService;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
OrdersService,
{ provide: UsersService, useValue: mockUsersService },
{ provide: InventoryService, useValue: mockInventoryService },
{ provide: PaymentService, useValue: mockPaymentService },
],
}).compile();
service = module.get(OrdersService);
});
});
// VALID: Factory pattern for dynamic instantiation
@Injectable()
export class HandlerFactory {
constructor(private moduleRef: ModuleRef) {}
getHandler(type: string): Handler {
switch (type) {
case 'email':
return this.moduleRef.get(EmailHandler);
case 'sms':
return this.moduleRef.get(SmsHandler);
default:
return this.moduleRef.get(DefaultHandler);
}
}
}
```
Reference: [NestJS Module Reference](https://docs.nestjs.com/fundamentals/module-ref)
@@ -0,0 +1,165 @@
---
title: Apply Interface Segregation Principle
impact: HIGH
impactDescription: Reduces coupling and improves testability by 30-50%
tags: dependency-injection, interfaces, solid, isp
---
## Apply Interface Segregation Principle
Clients should not be forced to depend on interfaces they don't use. In NestJS, this means keeping interfaces small and focused on specific capabilities rather than creating "fat" interfaces that bundle unrelated methods. When a service only needs to send emails, it shouldn't depend on an interface that also includes SMS, push notifications, and logging. Split large interfaces into role-based ones.
**Incorrect (fat interface forcing unused dependencies):**
```typescript
// Fat interface - forces all consumers to depend on everything
interface NotificationService {
sendEmail(to: string, subject: string, body: string): Promise<void>;
sendSms(phone: string, message: string): Promise<void>;
sendPush(userId: string, notification: PushPayload): Promise<void>;
sendSlack(channel: string, message: string): Promise<void>;
logNotification(type: string, payload: any): Promise<void>;
getDeliveryStatus(id: string): Promise<DeliveryStatus>;
retryFailed(id: string): Promise<void>;
scheduleNotification(dto: ScheduleDto): Promise<string>;
}
// Consumer only needs email, but must mock everything for tests
@Injectable()
export class OrdersService {
constructor(
private notifications: NotificationService, // Depends on 8 methods, uses 1
) {}
async confirmOrder(order: Order): Promise<void> {
await this.notifications.sendEmail(
order.customer.email,
'Order Confirmed',
`Your order ${order.id} has been confirmed.`,
);
}
}
// Testing is painful - must mock unused methods
const mockNotificationService = {
sendEmail: jest.fn(),
sendSms: jest.fn(), // Never used, but required
sendPush: jest.fn(), // Never used, but required
sendSlack: jest.fn(), // Never used, but required
logNotification: jest.fn(), // Never used, but required
getDeliveryStatus: jest.fn(), // Never used, but required
retryFailed: jest.fn(), // Never used, but required
scheduleNotification: jest.fn(), // Never used, but required
};
```
**Correct (segregated interfaces by capability):**
```typescript
// Segregated interfaces - each focused on one capability
interface EmailSender {
sendEmail(to: string, subject: string, body: string): Promise<void>;
}
interface SmsSender {
sendSms(phone: string, message: string): Promise<void>;
}
interface PushSender {
sendPush(userId: string, notification: PushPayload): Promise<void>;
}
interface NotificationLogger {
logNotification(type: string, payload: any): Promise<void>;
}
interface NotificationScheduler {
scheduleNotification(dto: ScheduleDto): Promise<string>;
}
// Implementation can implement multiple interfaces
@Injectable()
export class NotificationService implements EmailSender, SmsSender, PushSender {
async sendEmail(to: string, subject: string, body: string): Promise<void> {
// Email implementation
}
async sendSms(phone: string, message: string): Promise<void> {
// SMS implementation
}
async sendPush(userId: string, notification: PushPayload): Promise<void> {
// Push implementation
}
}
// Or separate implementations
@Injectable()
export class SendGridEmailService implements EmailSender {
async sendEmail(to: string, subject: string, body: string): Promise<void> {
// SendGrid-specific implementation
}
}
// Consumer depends only on what it needs
@Injectable()
export class OrdersService {
constructor(
@Inject(EMAIL_SENDER) private emailSender: EmailSender, // Minimal dependency
) {}
async confirmOrder(order: Order): Promise<void> {
await this.emailSender.sendEmail(
order.customer.email,
'Order Confirmed',
`Your order ${order.id} has been confirmed.`,
);
}
}
// Testing is simple - only mock what's used
const mockEmailSender: EmailSender = {
sendEmail: jest.fn(),
};
// Module registration with tokens
export const EMAIL_SENDER = Symbol('EMAIL_SENDER');
export const SMS_SENDER = Symbol('SMS_SENDER');
@Module({
providers: [
{ provide: EMAIL_SENDER, useClass: SendGridEmailService },
{ provide: SMS_SENDER, useClass: TwilioSmsService },
],
exports: [EMAIL_SENDER, SMS_SENDER],
})
export class NotificationModule {}
```
**Combining interfaces when needed:**
```typescript
// Sometimes a consumer legitimately needs multiple capabilities
interface EmailAndSmsSender extends EmailSender, SmsSender {}
// Or use intersection types
type MultiChannelSender = EmailSender & SmsSender & PushSender;
// Consumer that genuinely needs multiple channels
@Injectable()
export class AlertService {
constructor(
@Inject(MULTI_CHANNEL_SENDER)
private sender: EmailSender & SmsSender,
) {}
async sendCriticalAlert(user: User, message: string): Promise<void> {
await Promise.all([
this.sender.sendEmail(user.email, 'Critical Alert', message),
this.sender.sendSms(user.phone, message),
]);
}
}
```
Reference: [Interface Segregation Principle](https://en.wikipedia.org/wiki/Interface_segregation_principle)
@@ -0,0 +1,221 @@
---
title: Honor Liskov Substitution Principle
impact: HIGH
impactDescription: Ensures implementations are truly interchangeable without breaking callers
tags: dependency-injection, inheritance, solid, lsp
---
## Honor Liskov Substitution Principle
Subtypes must be substitutable for their base types without altering program correctness. In NestJS with dependency injection, this means any implementation of an interface or abstract class must honor the contract completely. A mock payment service used in tests must behave like a real payment service (return similar shapes, handle errors the same way). Violating LSP causes subtle bugs when swapping implementations.
**Incorrect (implementation violates the contract):**
```typescript
// Base interface with clear contract
interface PaymentGateway {
/**
* Charges the specified amount.
* @returns PaymentResult on success
* @throws PaymentFailedException on payment failure
*/
charge(amount: number, currency: string): Promise<PaymentResult>;
}
// Production implementation - follows the contract
@Injectable()
export class StripeService implements PaymentGateway {
async charge(amount: number, currency: string): Promise<PaymentResult> {
const response = await this.stripe.charges.create({ amount, currency });
return { success: true, transactionId: response.id, amount };
}
}
// Mock that violates LSP - different behavior!
@Injectable()
export class MockPaymentService implements PaymentGateway {
async charge(amount: number, currency: string): Promise<PaymentResult> {
// VIOLATION 1: Throws for valid input (contract says return PaymentResult)
if (amount > 1000) {
throw new Error('Mock does not support large amounts');
}
// VIOLATION 2: Returns null instead of PaymentResult
if (currency !== 'USD') {
return null as any; // Real service would convert or reject properly
}
// VIOLATION 3: Missing required field
return { success: true } as PaymentResult; // Missing transactionId!
}
}
// Consumer trusts the contract
@Injectable()
export class OrdersService {
constructor(@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway) {}
async checkout(order: Order): Promise<void> {
const result = await this.payment.charge(order.total, order.currency);
// These fail with MockPaymentService:
await this.saveTransaction(result.transactionId); // undefined!
await this.sendReceipt(result); // might be null!
}
}
```
**Correct (implementations honor the contract):**
```typescript
// Well-defined interface with documented behavior
interface PaymentGateway {
/**
* Charges the specified amount.
* @param amount - Amount in smallest currency unit (cents)
* @param currency - ISO 4217 currency code
* @returns PaymentResult with transactionId, success status, and amount
* @throws PaymentFailedException if charge is declined
* @throws InvalidCurrencyException if currency is not supported
*/
charge(amount: number, currency: string): Promise<PaymentResult>;
/**
* Refunds a previous charge.
* @throws TransactionNotFoundException if transactionId is invalid
*/
refund(transactionId: string, amount?: number): Promise<RefundResult>;
}
// Production implementation
@Injectable()
export class StripeService implements PaymentGateway {
async charge(amount: number, currency: string): Promise<PaymentResult> {
try {
const response = await this.stripe.charges.create({ amount, currency });
return {
success: true,
transactionId: response.id,
amount: response.amount,
};
} catch (error) {
if (error.type === 'card_error') {
throw new PaymentFailedException(error.message);
}
throw error;
}
}
async refund(transactionId: string, amount?: number): Promise<RefundResult> {
// Implementation...
}
}
// Mock that honors LSP - same contract, same behavior shape
@Injectable()
export class MockPaymentService implements PaymentGateway {
private transactions = new Map<string, PaymentResult>();
async charge(amount: number, currency: string): Promise<PaymentResult> {
// Honor the contract: validate currency like real service would
if (!['USD', 'EUR', 'GBP'].includes(currency)) {
throw new InvalidCurrencyException(`Unsupported currency: ${currency}`);
}
// Simulate decline for specific test scenarios
if (amount === 99999) {
throw new PaymentFailedException('Card declined (test scenario)');
}
// Return same shape as production
const result: PaymentResult = {
success: true,
transactionId: `mock_${Date.now()}_${Math.random().toString(36)}`,
amount,
};
this.transactions.set(result.transactionId, result);
return result;
}
async refund(transactionId: string, amount?: number): Promise<RefundResult> {
// Honor the contract: throw if transaction not found
if (!this.transactions.has(transactionId)) {
throw new TransactionNotFoundException(transactionId);
}
return {
success: true,
refundId: `refund_${transactionId}`,
amount: amount ?? this.transactions.get(transactionId)!.amount,
};
}
}
// Consumer can swap implementations safely
@Injectable()
export class OrdersService {
constructor(@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway) {}
async checkout(order: Order): Promise<Order> {
try {
const result = await this.payment.charge(order.total, order.currency);
// Works with both StripeService and MockPaymentService
order.transactionId = result.transactionId;
order.status = 'paid';
return order;
} catch (error) {
if (error instanceof PaymentFailedException) {
order.status = 'payment_failed';
return order;
}
throw error;
}
}
}
```
**Testing LSP compliance:**
```typescript
// Shared test suite that any implementation must pass
function testPaymentGatewayContract(
createGateway: () => PaymentGateway,
) {
describe('PaymentGateway contract', () => {
let gateway: PaymentGateway;
beforeEach(() => {
gateway = createGateway();
});
it('returns PaymentResult with all required fields', async () => {
const result = await gateway.charge(1000, 'USD');
expect(result).toHaveProperty('success');
expect(result).toHaveProperty('transactionId');
expect(result).toHaveProperty('amount');
expect(typeof result.transactionId).toBe('string');
});
it('throws InvalidCurrencyException for unsupported currency', async () => {
await expect(gateway.charge(1000, 'INVALID'))
.rejects.toThrow(InvalidCurrencyException);
});
it('throws TransactionNotFoundException for invalid refund', async () => {
await expect(gateway.refund('nonexistent'))
.rejects.toThrow(TransactionNotFoundException);
});
});
}
// Run against all implementations
describe('StripeService', () => {
testPaymentGatewayContract(() => new StripeService(mockStripeClient));
});
describe('MockPaymentService', () => {
testPaymentGatewayContract(() => new MockPaymentService());
});
```
Reference: [Liskov Substitution Principle](https://en.wikipedia.org/wiki/Liskov_substitution_principle)
@@ -0,0 +1,86 @@
---
title: Prefer Constructor Injection
impact: CRITICAL
impactDescription: Required for proper DI and testing
tags: dependency-injection, constructor, testing
---
## Prefer Constructor Injection
Always use constructor injection over property injection. Constructor injection makes dependencies explicit, enables TypeScript type checking, ensures dependencies are available when the class is instantiated, and improves testability. This is required for proper DI, testing, and TypeScript support.
**Incorrect (property injection with hidden dependencies):**
```typescript
// Property injection - avoid unless necessary
@Injectable()
export class UsersService {
@Inject()
private userRepo: UserRepository; // Hidden dependency
@Inject('CONFIG')
private config: ConfigType; // Also hidden
async findAll() {
return this.userRepo.find();
}
}
// Problems:
// 1. Dependencies not visible in constructor
// 2. Service can be instantiated without dependencies in tests
// 3. TypeScript can't enforce dependency types at instantiation
```
**Correct (constructor injection with explicit dependencies):**
```typescript
// Constructor injection - explicit and testable
@Injectable()
export class UsersService {
constructor(
private readonly userRepo: UserRepository,
@Inject('CONFIG') private readonly config: ConfigType,
) {}
async findAll(): Promise<User[]> {
return this.userRepo.find();
}
}
// Testing is straightforward
describe('UsersService', () => {
let service: UsersService;
let mockRepo: jest.Mocked<UserRepository>;
beforeEach(() => {
mockRepo = {
find: jest.fn(),
save: jest.fn(),
} as any;
service = new UsersService(mockRepo, { dbUrl: 'test' });
});
it('should find all users', async () => {
mockRepo.find.mockResolvedValue([{ id: '1', name: 'Test' }]);
const result = await service.findAll();
expect(result).toHaveLength(1);
});
});
// Only use property injection for optional dependencies
@Injectable()
export class LoggingService {
@Optional()
@Inject('ANALYTICS')
private analytics?: AnalyticsService;
log(message: string) {
console.log(message);
this.analytics?.track('log', message); // Optional enhancement
}
}
```
Reference: [NestJS Providers](https://docs.nestjs.com/providers)
@@ -0,0 +1,94 @@
---
title: Understand Provider Scopes
impact: CRITICAL
impactDescription: Prevents data leaks and performance issues
tags: dependency-injection, scopes, request-context
---
## Understand Provider Scopes
NestJS has three provider scopes: DEFAULT (singleton), REQUEST (per-request instance), and TRANSIENT (new instance for each injection). Most providers should be singletons. Request-scoped providers have performance implications as they bubble up through the dependency tree. Understanding scopes prevents memory leaks and incorrect data sharing.
**Incorrect (wrong scope usage):**
```typescript
// Request-scoped when not needed (performance hit)
@Injectable({ scope: Scope.REQUEST })
export class UsersService {
// This creates a new instance for EVERY request
// All dependencies also become request-scoped
async findAll() {
return this.userRepo.find();
}
}
// Singleton with mutable request state
@Injectable() // Default: singleton
export class RequestContextService {
private userId: string; // DANGER: Shared across all requests!
setUser(userId: string) {
this.userId = userId; // Overwrites for all concurrent requests
}
getUser() {
return this.userId; // Returns wrong user!
}
}
```
**Correct (appropriate scope for each use case):**
```typescript
// Singleton for stateless services (default, most common)
@Injectable()
export class UsersService {
constructor(private readonly userRepo: UserRepository) {}
async findById(id: string): Promise<User> {
return this.userRepo.findOne({ where: { id } });
}
}
// Request-scoped ONLY when you need request context
@Injectable({ scope: Scope.REQUEST })
export class RequestContextService {
private userId: string;
setUser(userId: string) {
this.userId = userId;
}
getUser(): string {
return this.userId;
}
}
// Better: Use NestJS built-in request context
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
@Injectable({ scope: Scope.REQUEST })
export class AuditService {
constructor(@Inject(REQUEST) private request: Request) {}
log(action: string) {
console.log(`User ${this.request.user?.id} performed ${action}`);
}
}
// Best: Use ClsModule for async context (no scope bubble-up)
import { ClsService } from 'nestjs-cls';
@Injectable() // Stays singleton!
export class AuditService {
constructor(private cls: ClsService) {}
log(action: string) {
const userId = this.cls.get('userId');
console.log(`User ${userId} performed ${action}`);
}
}
```
Reference: [NestJS Injection Scopes](https://docs.nestjs.com/fundamentals/injection-scopes)
@@ -0,0 +1,101 @@
---
title: Use Injection Tokens for Interfaces
impact: HIGH
impactDescription: Enables interface-based DI at runtime
tags: dependency-injection, tokens, interfaces
---
## Use Injection Tokens for Interfaces
TypeScript interfaces are erased at compile time and can't be used as injection tokens. Use string tokens, symbols, or abstract classes when you want to inject implementations of interfaces. This enables swapping implementations for testing or different environments.
**Incorrect (interface can't be used as token):**
```typescript
// Interface can't be used as injection token
interface PaymentGateway {
charge(amount: number): Promise<PaymentResult>;
}
@Injectable()
export class StripeService implements PaymentGateway {
charge(amount: number) { /* ... */ }
}
@Injectable()
export class OrdersService {
// This WON'T work - PaymentGateway doesn't exist at runtime
constructor(private payment: PaymentGateway) {}
}
```
**Correct (symbol tokens or abstract classes):**
```typescript
// Option 1: String/Symbol tokens (most flexible)
export const PAYMENT_GATEWAY = Symbol('PAYMENT_GATEWAY');
export interface PaymentGateway {
charge(amount: number): Promise<PaymentResult>;
}
@Injectable()
export class StripeService implements PaymentGateway {
async charge(amount: number): Promise<PaymentResult> {
// Stripe implementation
}
}
@Injectable()
export class MockPaymentService implements PaymentGateway {
async charge(amount: number): Promise<PaymentResult> {
return { success: true, id: 'mock-id' };
}
}
// Module registration
@Module({
providers: [
{
provide: PAYMENT_GATEWAY,
useClass: process.env.NODE_ENV === 'test'
? MockPaymentService
: StripeService,
},
],
exports: [PAYMENT_GATEWAY],
})
export class PaymentModule {}
// Injection
@Injectable()
export class OrdersService {
constructor(
@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway,
) {}
async createOrder(dto: CreateOrderDto) {
await this.payment.charge(dto.amount);
}
}
// Option 2: Abstract class (carries runtime type info)
export abstract class PaymentGateway {
abstract charge(amount: number): Promise<PaymentResult>;
}
@Injectable()
export class StripeService extends PaymentGateway {
async charge(amount: number): Promise<PaymentResult> {
// Implementation
}
}
// No @Inject needed with abstract class
@Injectable()
export class OrdersService {
constructor(private payment: PaymentGateway) {}
}
```
Reference: [NestJS Custom Providers](https://docs.nestjs.com/fundamentals/custom-providers)
@@ -0,0 +1,125 @@
---
title: Handle Async Errors Properly
impact: HIGH
impactDescription: Prevents process crashes from unhandled rejections
tags: error-handling, async, promises
---
## Handle Async Errors Properly
NestJS automatically catches errors from async route handlers, but errors from background tasks, event handlers, and manually created promises can crash your application. Always handle async errors explicitly and use global handlers as a safety net.
**Incorrect (fire-and-forget without error handling):**
```typescript
// Fire-and-forget without error handling
@Injectable()
export class UsersService {
async createUser(dto: CreateUserDto): Promise<User> {
const user = await this.repo.save(dto);
// Fire and forget - if this fails, error is unhandled!
this.emailService.sendWelcome(user.email);
return user;
}
}
// Unhandled promise in event handler
@Injectable()
export class OrdersService {
@OnEvent('order.created')
handleOrderCreated(event: OrderCreatedEvent) {
// This returns a promise but it's not awaited!
this.processOrder(event);
// Errors will crash the process
}
private async processOrder(event: OrderCreatedEvent): Promise<void> {
await this.inventoryService.reserve(event.items);
await this.notificationService.send(event.userId);
}
}
// Missing try-catch in scheduled tasks
@Cron('0 0 * * *')
async dailyCleanup(): Promise<void> {
await this.cleanupService.run();
// If this throws, no error handling
}
```
**Correct (explicit async error handling):**
```typescript
// Handle fire-and-forget with explicit catch
@Injectable()
export class UsersService {
private readonly logger = new Logger(UsersService.name);
async createUser(dto: CreateUserDto): Promise<User> {
const user = await this.repo.save(dto);
// Explicitly catch and log errors
this.emailService.sendWelcome(user.email).catch((error) => {
this.logger.error('Failed to send welcome email', error.stack);
// Optionally queue for retry
});
return user;
}
}
// Properly handle async event handlers
@Injectable()
export class OrdersService {
private readonly logger = new Logger(OrdersService.name);
@OnEvent('order.created')
async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
try {
await this.processOrder(event);
} catch (error) {
this.logger.error('Failed to process order', { event, error });
// Don't rethrow - would crash the process
await this.deadLetterQueue.add('order.created', event);
}
}
}
// Safe scheduled tasks
@Injectable()
export class CleanupService {
private readonly logger = new Logger(CleanupService.name);
@Cron('0 0 * * *')
async dailyCleanup(): Promise<void> {
try {
await this.cleanupService.run();
this.logger.log('Daily cleanup completed');
} catch (error) {
this.logger.error('Daily cleanup failed', error.stack);
// Alert or retry logic
}
}
}
// Global unhandled rejection handler in main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const logger = new Logger('Bootstrap');
process.on('unhandledRejection', (reason, promise) => {
logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
});
process.on('uncaughtException', (error) => {
logger.error('Uncaught Exception:', error);
process.exit(1);
});
await app.listen(3000);
}
```
Reference: [Node.js Unhandled Rejections](https://nodejs.org/api/process.html#event-unhandledrejection)
@@ -0,0 +1,114 @@
---
title: Throw HTTP Exceptions from Services
impact: HIGH
impactDescription: Keeps controllers thin and simplifies error handling
tags: error-handling, exceptions, services
---
## Throw HTTP Exceptions from Services
It's acceptable (and often preferable) to throw `HttpException` subclasses from services in HTTP applications. This keeps controllers thin and allows services to communicate appropriate error states. For truly layer-agnostic services, use domain exceptions that map to HTTP status codes.
**Incorrect (return error objects instead of throwing):**
```typescript
// Return error objects instead of throwing
@Injectable()
export class UsersService {
async findById(id: string): Promise<{ user?: User; error?: string }> {
const user = await this.repo.findOne({ where: { id } });
if (!user) {
return { error: 'User not found' }; // Controller must check this
}
return { user };
}
}
@Controller('users')
export class UsersController {
@Get(':id')
async findOne(@Param('id') id: string) {
const result = await this.usersService.findById(id);
if (result.error) {
throw new NotFoundException(result.error);
}
return result.user;
}
}
```
**Correct (throw exceptions directly from service):**
```typescript
// Throw exceptions directly from service
@Injectable()
export class UsersService {
constructor(private readonly repo: UserRepository) {}
async findById(id: string): Promise<User> {
const user = await this.repo.findOne({ where: { id } });
if (!user) {
throw new NotFoundException(`User #${id} not found`);
}
return user;
}
async create(dto: CreateUserDto): Promise<User> {
const existing = await this.repo.findOne({
where: { email: dto.email },
});
if (existing) {
throw new ConflictException('Email already registered');
}
return this.repo.save(dto);
}
async update(id: string, dto: UpdateUserDto): Promise<User> {
const user = await this.findById(id); // Throws if not found
Object.assign(user, dto);
return this.repo.save(user);
}
}
// Controller stays thin
@Controller('users')
export class UsersController {
@Get(':id')
findOne(@Param('id') id: string): Promise<User> {
return this.usersService.findById(id);
}
@Post()
create(@Body() dto: CreateUserDto): Promise<User> {
return this.usersService.create(dto);
}
}
// For layer-agnostic services, use domain exceptions
export class EntityNotFoundException extends Error {
constructor(
public readonly entity: string,
public readonly id: string,
) {
super(`${entity} with ID "${id}" not found`);
}
}
// Map to HTTP in exception filter
@Catch(EntityNotFoundException)
export class EntityNotFoundFilter implements ExceptionFilter {
catch(exception: EntityNotFoundException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
response.status(404).json({
statusCode: 404,
message: exception.message,
entity: exception.entity,
id: exception.id,
});
}
}
```
Reference: [NestJS Exception Filters](https://docs.nestjs.com/exception-filters)
@@ -0,0 +1,140 @@
---
title: Use Exception Filters for Error Handling
impact: HIGH
impactDescription: Consistent, centralized error handling
tags: error-handling, exception-filters, consistency
---
## Use Exception Filters for Error Handling
Never catch exceptions and manually format error responses in controllers. Use NestJS exception filters to handle errors consistently across your application. Create custom exception filters for specific error types and a global filter for unhandled exceptions.
**Incorrect (manual error handling in controllers):**
```typescript
// Manual error handling in controllers
@Controller('users')
export class UsersController {
@Get(':id')
async findOne(@Param('id') id: string, @Res() res: Response) {
try {
const user = await this.usersService.findById(id);
if (!user) {
return res.status(404).json({
statusCode: 404,
message: 'User not found',
});
}
return res.json(user);
} catch (error) {
console.error(error);
return res.status(500).json({
statusCode: 500,
message: 'Internal server error',
});
}
}
}
```
**Correct (exception filters with consistent handling):**
```typescript
// Use built-in and custom exceptions
@Controller('users')
export class UsersController {
@Get(':id')
async findOne(@Param('id') id: string): Promise<User> {
const user = await this.usersService.findById(id);
if (!user) {
throw new NotFoundException(`User #${id} not found`);
}
return user;
}
}
// Custom domain exception
export class UserNotFoundException extends NotFoundException {
constructor(userId: string) {
super({
statusCode: 404,
error: 'Not Found',
message: `User with ID "${userId}" not found`,
code: 'USER_NOT_FOUND',
});
}
}
// Custom exception filter for domain errors
@Catch(DomainException)
export class DomainExceptionFilter implements ExceptionFilter {
catch(exception: DomainException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus?.() || 400;
response.status(status).json({
statusCode: status,
code: exception.code,
message: exception.message,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
// Global exception filter for unhandled errors
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
constructor(private readonly logger: Logger) {}
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message =
exception instanceof HttpException
? exception.message
: 'Internal server error';
this.logger.error(
`${request.method} ${request.url}`,
exception instanceof Error ? exception.stack : exception,
);
response.status(status).json({
statusCode: status,
message,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
// Register globally in main.ts
app.useGlobalFilters(
new AllExceptionsFilter(app.get(Logger)),
new DomainExceptionFilter(),
);
// Or via module
@Module({
providers: [
{
provide: APP_FILTER,
useClass: AllExceptionsFilter,
},
],
})
export class AppModule {}
```
Reference: [NestJS Exception Filters](https://docs.nestjs.com/exception-filters)
@@ -0,0 +1,226 @@
---
title: Implement Health Checks for Microservices
impact: MEDIUM-HIGH
impactDescription: Health checks enable orchestrators to manage service lifecycle
tags: microservices, health-checks, terminus, kubernetes
---
## Implement Health Checks for Microservices
Implement liveness and readiness probes using `@nestjs/terminus`. Liveness checks determine if the service should be restarted. Readiness checks determine if the service can accept traffic. Proper health checks enable Kubernetes and load balancers to route traffic correctly.
**Incorrect (simple ping that doesn't check dependencies):**
```typescript
// Simple ping that doesn't check dependencies
@Controller('health')
export class HealthController {
@Get()
check(): string {
return 'OK'; // Service might be unhealthy but returns OK
}
}
// Health check that blocks on slow dependencies
@Controller('health')
export class HealthController {
@Get()
async check(): Promise<string> {
// If database is slow, health check times out
await this.userRepo.findOne({ where: { id: '1' } });
await this.redis.ping();
await this.externalApi.healthCheck();
return 'OK';
}
}
```
**Correct (use @nestjs/terminus for comprehensive health checks):**
```typescript
// Use @nestjs/terminus for comprehensive health checks
import {
HealthCheckService,
HttpHealthIndicator,
TypeOrmHealthIndicator,
HealthCheck,
DiskHealthIndicator,
MemoryHealthIndicator,
} from '@nestjs/terminus';
@Controller('health')
export class HealthController {
constructor(
private health: HealthCheckService,
private http: HttpHealthIndicator,
private db: TypeOrmHealthIndicator,
private disk: DiskHealthIndicator,
private memory: MemoryHealthIndicator,
) {}
// Liveness probe - is the service alive?
@Get('live')
@HealthCheck()
liveness() {
return this.health.check([
// Basic checks only
() => this.memory.checkHeap('memory_heap', 200 * 1024 * 1024), // 200MB
]);
}
// Readiness probe - can the service handle traffic?
@Get('ready')
@HealthCheck()
readiness() {
return this.health.check([
() => this.db.pingCheck('database'),
() =>
this.http.pingCheck('redis', 'http://redis:6379', { timeout: 1000 }),
() =>
this.disk.checkStorage('disk', { path: '/', thresholdPercent: 0.9 }),
]);
}
// Deep health check for debugging
@Get('deep')
@HealthCheck()
deepCheck() {
return this.health.check([
() => this.db.pingCheck('database'),
() => this.memory.checkHeap('memory_heap', 200 * 1024 * 1024),
() => this.memory.checkRSS('memory_rss', 300 * 1024 * 1024),
() =>
this.disk.checkStorage('disk', { path: '/', thresholdPercent: 0.9 }),
() =>
this.http.pingCheck('external-api', 'https://api.example.com/health'),
]);
}
}
// Custom indicator for business-specific health
@Injectable()
export class QueueHealthIndicator extends HealthIndicator {
constructor(private queueService: QueueService) {
super();
}
async isHealthy(key: string): Promise<HealthIndicatorResult> {
const queueStats = await this.queueService.getStats();
const isHealthy = queueStats.failedCount < 100;
const result = this.getStatus(key, isHealthy, {
waiting: queueStats.waitingCount,
active: queueStats.activeCount,
failed: queueStats.failedCount,
});
if (!isHealthy) {
throw new HealthCheckError('Queue unhealthy', result);
}
return result;
}
}
// Redis health indicator
@Injectable()
export class RedisHealthIndicator extends HealthIndicator {
constructor(@InjectRedis() private redis: Redis) {
super();
}
async isHealthy(key: string): Promise<HealthIndicatorResult> {
try {
const pong = await this.redis.ping();
return this.getStatus(key, pong === 'PONG');
} catch (error) {
throw new HealthCheckError('Redis check failed', this.getStatus(key, false));
}
}
}
// Use custom indicators
@Get('ready')
@HealthCheck()
readiness() {
return this.health.check([
() => this.db.pingCheck('database'),
() => this.redis.isHealthy('redis'),
() => this.queue.isHealthy('job-queue'),
]);
}
// Graceful shutdown handling
@Injectable()
export class GracefulShutdownService implements OnApplicationShutdown {
private isShuttingDown = false;
isShutdown(): boolean {
return this.isShuttingDown;
}
async onApplicationShutdown(signal: string): Promise<void> {
this.isShuttingDown = true;
console.log(`Shutting down on ${signal}`);
// Wait for in-flight requests
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
// Health check respects shutdown state
@Get('ready')
@HealthCheck()
readiness() {
if (this.shutdownService.isShutdown()) {
throw new ServiceUnavailableException('Shutting down');
}
return this.health.check([
() => this.db.pingCheck('database'),
]);
}
```
### Kubernetes Configuration
```yaml
# Kubernetes deployment with probes
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-service
spec:
template:
spec:
containers:
- name: api
image: api-service:latest
ports:
- containerPort: 3000
livenessProbe:
httpGet:
path: /health/live
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
startupProbe:
httpGet:
path: /health/live
port: 3000
initialDelaySeconds: 0
periodSeconds: 5
failureThreshold: 30
```
Reference: [NestJS Terminus](https://docs.nestjs.com/recipes/terminus)
@@ -0,0 +1,167 @@
---
title: Use Message and Event Patterns Correctly
impact: MEDIUM
impactDescription: Proper patterns ensure reliable microservice communication
tags: microservices, message-pattern, event-pattern, communication
---
## Use Message and Event Patterns Correctly
NestJS microservices support two communication patterns: request-response (MessagePattern) and event-based (EventPattern). Use MessagePattern when you need a response, and EventPattern for fire-and-forget notifications. Understanding the difference prevents communication bugs.
**Incorrect (using wrong pattern for use case):**
```typescript
// Use @MessagePattern for fire-and-forget
@Controller()
export class NotificationsController {
@MessagePattern('user.created')
async handleUserCreated(data: UserCreatedEvent) {
// This WAITS for response, blocking the sender
await this.emailService.sendWelcome(data.email);
// If email fails, sender gets an error (coupling!)
}
}
// Use @EventPattern expecting a response
@Controller()
export class OrdersController {
@EventPattern('inventory.check')
async checkInventory(data: CheckInventoryDto) {
const available = await this.inventory.check(data);
return available; // This return value is IGNORED with @EventPattern!
}
}
// Tight coupling in client
@Injectable()
export class UsersService {
async createUser(dto: CreateUserDto): Promise<User> {
const user = await this.repo.save(dto);
// Blocks until notification service responds
await this.client.send('user.created', user).toPromise();
// If notification service is down, user creation fails!
return user;
}
}
```
**Correct (use MessagePattern for request-response, EventPattern for fire-and-forget):**
```typescript
// MessagePattern: Request-Response (when you NEED a response)
@Controller()
export class InventoryController {
@MessagePattern({ cmd: 'check_inventory' })
async checkInventory(data: CheckInventoryDto): Promise<InventoryResult> {
const result = await this.inventoryService.check(data.productId, data.quantity);
return result; // Response sent back to caller
}
}
// Client expects response
@Injectable()
export class OrdersService {
async createOrder(dto: CreateOrderDto): Promise<Order> {
// Check inventory - we NEED this response to proceed
const inventory = await firstValueFrom(
this.inventoryClient.send<InventoryResult>(
{ cmd: 'check_inventory' },
{ productId: dto.productId, quantity: dto.quantity },
),
);
if (!inventory.available) {
throw new BadRequestException('Insufficient inventory');
}
return this.repo.save(dto);
}
}
// EventPattern: Fire-and-Forget (for notifications, side effects)
@Controller()
export class NotificationsController {
@EventPattern('user.created')
async handleUserCreated(data: UserCreatedEvent): Promise<void> {
// No return value needed - just process the event
await this.emailService.sendWelcome(data.email);
await this.analyticsService.track('user_signup', data);
// If this fails, it doesn't affect the sender
}
}
// Client emits event without waiting
@Injectable()
export class UsersService {
async createUser(dto: CreateUserDto): Promise<User> {
const user = await this.repo.save(dto);
// Fire and forget - doesn't block, doesn't wait
this.eventClient.emit('user.created', {
userId: user.id,
email: user.email,
timestamp: new Date(),
});
return user; // User creation succeeds regardless of event handling
}
}
// Hybrid pattern for critical events
@Injectable()
export class OrdersService {
async createOrder(dto: CreateOrderDto): Promise<Order> {
const order = await this.repo.save(dto);
// Critical: inventory reservation (use MessagePattern)
const reserved = await firstValueFrom(
this.inventoryClient.send({ cmd: 'reserve_inventory' }, {
orderId: order.id,
items: dto.items,
}),
);
if (!reserved.success) {
await this.repo.delete(order.id);
throw new BadRequestException('Could not reserve inventory');
}
// Non-critical: notifications (use EventPattern)
this.eventClient.emit('order.created', {
orderId: order.id,
userId: dto.userId,
total: dto.total,
});
return order;
}
}
// Error handling patterns
// MessagePattern errors propagate to caller
@MessagePattern({ cmd: 'get_user' })
async getUser(userId: string): Promise<User> {
const user = await this.repo.findOne({ where: { id: userId } });
if (!user) {
throw new RpcException('User not found'); // Received by caller
}
return user;
}
// EventPattern errors should be handled locally
@EventPattern('order.created')
async handleOrderCreated(data: OrderCreatedEvent): Promise<void> {
try {
await this.processOrder(data);
} catch (error) {
// Log and potentially retry - don't throw
this.logger.error('Failed to process order event', error);
await this.deadLetterQueue.add(data);
}
}
```
Reference: [NestJS Microservices](https://docs.nestjs.com/microservices/basics)
@@ -0,0 +1,252 @@
---
title: Use Message Queues for Background Jobs
impact: MEDIUM-HIGH
impactDescription: Queues enable reliable background processing
tags: microservices, queues, bullmq, background-jobs
---
## Use Message Queues for Background Jobs
Use `@nestjs/bullmq` for background job processing. Queues decouple long-running tasks from HTTP requests, enable retry logic, and distribute workload across workers. Use them for emails, file processing, notifications, and any task that shouldn't block user requests.
**Incorrect (long-running tasks in HTTP handlers):**
```typescript
// Long-running tasks in HTTP handlers
@Controller('reports')
export class ReportsController {
@Post()
async generate(@Body() dto: GenerateReportDto): Promise<Report> {
// This blocks the request for potentially minutes
const data = await this.fetchLargeDataset(dto);
const report = await this.processData(data); // Slow!
await this.sendEmail(dto.email, report); // Can fail!
return report; // Client times out
}
}
// Fire-and-forget without retry
@Injectable()
export class EmailService {
async sendWelcome(email: string): Promise<void> {
// If this fails, email is never sent
await this.mailer.send({ to: email, template: 'welcome' });
// No retry, no tracking, no visibility
}
}
// Use setInterval for scheduled tasks
setInterval(async () => {
await cleanupOldRecords();
}, 60000); // No error handling, memory leaks
```
**Correct (use BullMQ for background processing):**
```typescript
// Configure BullMQ
import { BullModule } from '@nestjs/bullmq';
@Module({
imports: [
BullModule.forRoot({
connection: {
host: 'localhost',
port: 6379,
},
defaultJobOptions: {
removeOnComplete: 1000,
removeOnFail: 5000,
attempts: 3,
backoff: {
type: 'exponential',
delay: 1000,
},
},
}),
BullModule.registerQueue(
{ name: 'email' },
{ name: 'reports' },
{ name: 'notifications' },
),
],
})
export class QueueModule {}
// Producer: Add jobs to queue
@Injectable()
export class ReportsService {
constructor(
@InjectQueue('reports') private reportsQueue: Queue,
) {}
async requestReport(dto: GenerateReportDto): Promise<{ jobId: string }> {
// Return immediately, process in background
const job = await this.reportsQueue.add('generate', dto, {
priority: dto.urgent ? 1 : 10,
delay: dto.scheduledFor ? Date.parse(dto.scheduledFor) - Date.now() : 0,
});
return { jobId: job.id };
}
async getJobStatus(jobId: string): Promise<JobStatus> {
const job = await this.reportsQueue.getJob(jobId);
return {
status: await job.getState(),
progress: job.progress,
result: job.returnvalue,
};
}
}
// Consumer: Process jobs
@Processor('reports')
export class ReportsProcessor {
private readonly logger = new Logger(ReportsProcessor.name);
@Process('generate')
async generateReport(job: Job<GenerateReportDto>): Promise<Report> {
this.logger.log(`Processing report job ${job.id}`);
// Update progress
await job.updateProgress(10);
const data = await this.fetchData(job.data);
await job.updateProgress(50);
const report = await this.processData(data);
await job.updateProgress(90);
await this.saveReport(report);
await job.updateProgress(100);
return report;
}
@OnQueueActive()
onActive(job: Job) {
this.logger.log(`Processing job ${job.id}`);
}
@OnQueueCompleted()
onCompleted(job: Job, result: any) {
this.logger.log(`Job ${job.id} completed`);
}
@OnQueueFailed()
onFailed(job: Job, error: Error) {
this.logger.error(`Job ${job.id} failed: ${error.message}`);
}
}
// Email queue with retry
@Processor('email')
export class EmailProcessor {
@Process('send')
async sendEmail(job: Job<SendEmailDto>): Promise<void> {
const { to, template, data } = job.data;
try {
await this.mailer.send({
to,
template,
context: data,
});
} catch (error) {
// BullMQ will retry based on job options
throw error;
}
}
}
// Usage
@Injectable()
export class NotificationService {
constructor(@InjectQueue('email') private emailQueue: Queue) {}
async sendWelcome(user: User): Promise<void> {
await this.emailQueue.add(
'send',
{
to: user.email,
template: 'welcome',
data: { name: user.name },
},
{
attempts: 5,
backoff: { type: 'exponential', delay: 5000 },
},
);
}
}
// Scheduled jobs
@Injectable()
export class ScheduledJobsService implements OnModuleInit {
constructor(@InjectQueue('maintenance') private queue: Queue) {}
async onModuleInit(): Promise<void> {
// Clean up old reports daily at midnight
await this.queue.add(
'cleanup',
{},
{
repeat: { cron: '0 0 * * *' },
jobId: 'daily-cleanup', // Prevent duplicates
},
);
// Send digest every hour
await this.queue.add(
'digest',
{},
{
repeat: { every: 60 * 60 * 1000 },
jobId: 'hourly-digest',
},
);
}
}
@Processor('maintenance')
export class MaintenanceProcessor {
@Process('cleanup')
async cleanup(): Promise<void> {
await this.cleanupOldReports();
await this.cleanupExpiredSessions();
}
@Process('digest')
async sendDigest(): Promise<void> {
const users = await this.getUsersForDigest();
for (const user of users) {
await this.emailQueue.add('send', { to: user.email, template: 'digest' });
}
}
}
// Queue monitoring with Bull Board
import { BullBoardModule } from '@bull-board/nestjs';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
@Module({
imports: [
BullBoardModule.forRoot({
route: '/admin/queues',
adapter: ExpressAdapter,
}),
BullBoardModule.forFeature({
name: 'email',
adapter: BullMQAdapter,
}),
BullBoardModule.forFeature({
name: 'reports',
adapter: BullMQAdapter,
}),
],
})
export class AdminModule {}
```
Reference: [NestJS Queues](https://docs.nestjs.com/techniques/queues)
@@ -0,0 +1,109 @@
---
title: Use Async Lifecycle Hooks Correctly
impact: HIGH
impactDescription: Improper async handling blocks application startup
tags: performance, lifecycle, async, hooks
---
## Use Async Lifecycle Hooks Correctly
NestJS lifecycle hooks (`onModuleInit`, `onApplicationBootstrap`, etc.) support async operations. However, misusing them can block application startup or cause race conditions. Understand the lifecycle order and use hooks appropriately.
**Incorrect (fire-and-forget async without await):**
```typescript
// Fire-and-forget async without await
@Injectable()
export class DatabaseService implements OnModuleInit {
onModuleInit() {
// This runs but doesn't block - app starts before DB is ready!
this.connect();
}
private async connect() {
await this.pool.connect();
console.log('Database connected');
}
}
// Heavy blocking operations in constructor
@Injectable()
export class ConfigService {
private config: Config;
constructor() {
// BLOCKS entire module instantiation synchronously
this.config = fs.readFileSync('config.json');
}
}
```
**Correct (return promises from async hooks):**
```typescript
// Return promise from async hooks
@Injectable()
export class DatabaseService implements OnModuleInit {
private pool: Pool;
async onModuleInit(): Promise<void> {
// NestJS waits for this to complete before continuing
await this.pool.connect();
console.log('Database connected');
}
async onModuleDestroy(): Promise<void> {
// Clean up resources on shutdown
await this.pool.end();
console.log('Database disconnected');
}
}
// Use onApplicationBootstrap for cross-module dependencies
@Injectable()
export class CacheWarmerService implements OnApplicationBootstrap {
constructor(
private cache: CacheService,
private products: ProductsService,
) {}
async onApplicationBootstrap(): Promise<void> {
// All modules are initialized, safe to warm cache
const products = await this.products.findPopular();
await this.cache.warmup(products);
}
}
// Heavy init in async hooks, not constructor
@Injectable()
export class ConfigService implements OnModuleInit {
private config: Config;
constructor() {
// Keep constructor synchronous and fast
}
async onModuleInit(): Promise<void> {
// Async loading in lifecycle hook
this.config = await this.loadConfig();
}
private async loadConfig(): Promise<Config> {
const file = await fs.promises.readFile('config.json');
return JSON.parse(file.toString());
}
get<T>(key: string): T {
return this.config[key];
}
}
// Enable shutdown hooks in main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableShutdownHooks(); // Enable SIGTERM/SIGINT handling
await app.listen(3000);
}
```
Reference: [NestJS Lifecycle Events](https://docs.nestjs.com/fundamentals/lifecycle-events)
@@ -0,0 +1,121 @@
---
title: Use Lazy Loading for Large Modules
impact: MEDIUM
impactDescription: Improves startup time for large applications
tags: performance, lazy-loading, modules, optimization
---
## Use Lazy Loading for Large Modules
NestJS supports lazy-loading modules, which defers initialization until first use. This is valuable for large applications where some features are rarely used, serverless deployments where cold start time matters, or when certain modules have heavy initialization costs.
**Incorrect (loading everything eagerly):**
```typescript
// Load everything eagerly in a large app
@Module({
imports: [
UsersModule,
OrdersModule,
PaymentsModule,
ReportsModule, // Heavy, rarely used
AnalyticsModule, // Heavy, rarely used
AdminModule, // Only admins use this
LegacyModule, // Migration module, rarely used
BulkImportModule, // Used once a month
],
})
export class AppModule {}
// All modules initialize at startup, even if never used
// Slow cold starts in serverless
// Memory wasted on unused modules
```
**Correct (lazy load rarely-used modules):**
```typescript
// Use LazyModuleLoader for optional modules
import { LazyModuleLoader } from '@nestjs/core';
@Injectable()
export class ReportsService {
constructor(private lazyModuleLoader: LazyModuleLoader) {}
async generateReport(type: string): Promise<Report> {
// Load module only when needed
const { ReportsModule } = await import('./reports/reports.module');
const moduleRef = await this.lazyModuleLoader.load(() => ReportsModule);
const reportsService = moduleRef.get(ReportsGeneratorService);
return reportsService.generate(type);
}
}
// Lazy load admin features with caching
@Injectable()
export class AdminService {
private adminModule: ModuleRef | null = null;
constructor(private lazyModuleLoader: LazyModuleLoader) {}
private async getAdminModule(): Promise<ModuleRef> {
if (!this.adminModule) {
const { AdminModule } = await import('./admin/admin.module');
this.adminModule = await this.lazyModuleLoader.load(() => AdminModule);
}
return this.adminModule;
}
async runAdminTask(task: string): Promise<void> {
const moduleRef = await this.getAdminModule();
const taskRunner = moduleRef.get(AdminTaskRunner);
await taskRunner.run(task);
}
}
// Reusable lazy loader service
@Injectable()
export class ModuleLoaderService {
private loadedModules = new Map<string, ModuleRef>();
constructor(private lazyModuleLoader: LazyModuleLoader) {}
async load<T>(
key: string,
importFn: () => Promise<{ default: Type<T> } | Type<T>>,
): Promise<ModuleRef> {
if (!this.loadedModules.has(key)) {
const module = await importFn();
const moduleType = 'default' in module ? module.default : module;
const moduleRef = await this.lazyModuleLoader.load(() => moduleType);
this.loadedModules.set(key, moduleRef);
}
return this.loadedModules.get(key)!;
}
}
// Preload modules in background after startup
@Injectable()
export class ModulePreloader implements OnApplicationBootstrap {
constructor(private lazyModuleLoader: LazyModuleLoader) {}
async onApplicationBootstrap(): Promise<void> {
setTimeout(async () => {
await this.preloadModule(() => import('./reports/reports.module'));
}, 5000); // 5 seconds after startup
}
private async preloadModule(importFn: () => Promise<any>): Promise<void> {
try {
const module = await importFn();
const moduleType = module.default || Object.values(module)[0];
await this.lazyModuleLoader.load(() => moduleType);
} catch (error) {
console.warn('Failed to preload module', error);
}
}
}
```
Reference: [NestJS Lazy Loading Modules](https://docs.nestjs.com/fundamentals/lazy-loading-modules)
@@ -0,0 +1,131 @@
---
title: Optimize Database Queries
impact: HIGH
impactDescription: Database queries are typically the largest source of latency
tags: performance, database, queries, optimization
---
## Optimize Database Queries
Select only needed columns, use proper indexes, avoid over-fetching relations, and consider query performance when designing your data access. Most API slowness traces back to inefficient database queries.
**Incorrect (over-fetching data and missing indexes):**
```typescript
// Select everything when you need few fields
@Injectable()
export class UsersService {
async findAllEmails(): Promise<string[]> {
const users = await this.repo.find();
// Fetches ALL columns for ALL users
return users.map((u) => u.email);
}
async getUserSummary(id: string): Promise<UserSummary> {
const user = await this.repo.findOne({
where: { id },
relations: ['posts', 'posts.comments', 'posts.comments.author', 'followers'],
});
// Over-fetches massive relation tree
return { name: user.name, postCount: user.posts.length };
}
}
// No indexes on frequently queried columns
@Entity()
export class Order {
@Column()
userId: string; // No index - full table scan on every lookup
@Column()
status: string; // No index - slow status filtering
}
```
**Correct (select only needed data with proper indexes):**
```typescript
// Select only needed columns
@Injectable()
export class UsersService {
async findAllEmails(): Promise<string[]> {
const users = await this.repo.find({
select: ['email'], // Only fetch email column
});
return users.map((u) => u.email);
}
// Use QueryBuilder for complex selections
async getUserSummary(id: string): Promise<UserSummary> {
return this.repo
.createQueryBuilder('user')
.select('user.name', 'name')
.addSelect('COUNT(post.id)', 'postCount')
.leftJoin('user.posts', 'post')
.where('user.id = :id', { id })
.groupBy('user.id')
.getRawOne();
}
// Fetch relations only when needed
async getFullProfile(id: string): Promise<User> {
return this.repo.findOne({
where: { id },
relations: ['posts'], // Only immediate relation
select: {
id: true,
name: true,
email: true,
posts: {
id: true,
title: true,
},
},
});
}
}
// Add indexes on frequently queried columns
@Entity()
@Index(['userId'])
@Index(['status'])
@Index(['createdAt'])
@Index(['userId', 'status']) // Composite index for common query pattern
export class Order {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
userId: string;
@Column()
status: string;
@CreateDateColumn()
createdAt: Date;
}
// Always paginate large datasets
@Injectable()
export class OrdersService {
async findAll(page = 1, limit = 20): Promise<PaginatedResult<Order>> {
const [items, total] = await this.repo.findAndCount({
skip: (page - 1) * limit,
take: limit,
order: { createdAt: 'DESC' },
});
return {
items,
meta: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
};
}
}
```
Reference: [TypeORM Query Builder](https://typeorm.io/select-query-builder)
@@ -0,0 +1,128 @@
---
title: Use Caching Strategically
impact: HIGH
impactDescription: Dramatically reduces database load and response times
tags: performance, caching, redis, optimization
---
## Use Caching Strategically
Implement caching for expensive operations, frequently accessed data, and external API calls. Use NestJS CacheModule with appropriate TTLs and cache invalidation strategies. Don't cache everything - focus on high-impact areas.
**Incorrect (no caching or caching everything):**
```typescript
// No caching for expensive, repeated queries
@Injectable()
export class ProductsService {
async getPopular(): Promise<Product[]> {
// Runs complex aggregation query EVERY request
return this.productsRepo
.createQueryBuilder('p')
.leftJoin('p.orders', 'o')
.select('p.*, COUNT(o.id) as orderCount')
.groupBy('p.id')
.orderBy('orderCount', 'DESC')
.limit(20)
.getMany();
}
}
// Cache everything without thought
@Injectable()
export class UsersService {
@CacheKey('users')
@CacheTTL(3600)
@UseInterceptors(CacheInterceptor)
async findAll(): Promise<User[]> {
// Caching user list for 1 hour is wrong if data changes frequently
return this.usersRepo.find();
}
}
```
**Correct (strategic caching with proper invalidation):**
```typescript
// Setup caching module
@Module({
imports: [
CacheModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
stores: [
new KeyvRedis(config.get('REDIS_URL')),
],
ttl: 60 * 1000, // Default 60s
}),
}),
],
})
export class AppModule {}
// Manual caching for granular control
@Injectable()
export class ProductsService {
constructor(
@Inject(CACHE_MANAGER) private cache: Cache,
private productsRepo: ProductRepository,
) {}
async getPopular(): Promise<Product[]> {
const cacheKey = 'products:popular';
// Try cache first
const cached = await this.cache.get<Product[]>(cacheKey);
if (cached) return cached;
// Cache miss - fetch and cache
const products = await this.fetchPopularProducts();
await this.cache.set(cacheKey, products, 5 * 60 * 1000); // 5 min TTL
return products;
}
// Invalidate cache on changes
async updateProduct(id: string, dto: UpdateProductDto): Promise<Product> {
const product = await this.productsRepo.save({ id, ...dto });
await this.cache.del('products:popular'); // Invalidate
return product;
}
}
// Decorator-based caching with auto-interceptor
@Controller('categories')
@UseInterceptors(CacheInterceptor)
export class CategoriesController {
@Get()
@CacheTTL(30 * 60 * 1000) // 30 minutes - categories rarely change
findAll(): Promise<Category[]> {
return this.categoriesService.findAll();
}
@Get(':id')
@CacheTTL(60 * 1000) // 1 minute
@CacheKey('category')
findOne(@Param('id') id: string): Promise<Category> {
return this.categoriesService.findOne(id);
}
}
// Event-based cache invalidation
@Injectable()
export class CacheInvalidationService {
constructor(@Inject(CACHE_MANAGER) private cache: Cache) {}
@OnEvent('product.created')
@OnEvent('product.updated')
@OnEvent('product.deleted')
async invalidateProductCaches(event: ProductEvent) {
await Promise.all([
this.cache.del('products:popular'),
this.cache.del(`product:${event.productId}`),
]);
}
}
```
Reference: [NestJS Caching](https://docs.nestjs.com/techniques/caching)
@@ -0,0 +1,146 @@
---
title: Implement Secure JWT Authentication
impact: CRITICAL
impactDescription: Essential for secure APIs
tags: security, jwt, authentication, tokens
---
## Implement Secure JWT Authentication
Use `@nestjs/jwt` with `@nestjs/passport` for authentication. Store secrets securely, use appropriate token lifetimes, implement refresh tokens, and validate tokens properly. Never expose sensitive data in JWT payloads.
**Incorrect (insecure JWT implementation):**
```typescript
// Hardcode secrets
@Module({
imports: [
JwtModule.register({
secret: 'my-secret-key', // Exposed in code
signOptions: { expiresIn: '7d' }, // Too long
}),
],
})
export class AuthModule {}
// Store sensitive data in JWT
async login(user: User): Promise<{ accessToken: string }> {
const payload = {
sub: user.id,
email: user.email,
password: user.password, // NEVER include password!
ssn: user.ssn, // NEVER include sensitive data!
isAdmin: user.isAdmin, // Can be tampered if not verified
};
return { accessToken: this.jwtService.sign(payload) };
}
// Skip token validation
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: 'my-secret',
});
}
async validate(payload: any): Promise<any> {
return payload; // No validation of user existence
}
}
```
**Correct (secure JWT with refresh tokens):**
```typescript
// Secure JWT configuration
@Module({
imports: [
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
secret: config.get<string>('JWT_SECRET'),
signOptions: {
expiresIn: '15m', // Short-lived access tokens
issuer: config.get<string>('JWT_ISSUER'),
audience: config.get<string>('JWT_AUDIENCE'),
},
}),
}),
PassportModule.register({ defaultStrategy: 'jwt' }),
],
})
export class AuthModule {}
// Minimal JWT payload
@Injectable()
export class AuthService {
async login(user: User): Promise<TokenResponse> {
// Only include necessary, non-sensitive data
const payload: JwtPayload = {
sub: user.id,
email: user.email,
roles: user.roles,
iat: Math.floor(Date.now() / 1000),
};
const accessToken = this.jwtService.sign(payload);
const refreshToken = await this.createRefreshToken(user.id);
return { accessToken, refreshToken, expiresIn: 900 };
}
private async createRefreshToken(userId: string): Promise<string> {
const token = randomBytes(32).toString('hex');
const hashedToken = await bcrypt.hash(token, 10);
await this.refreshTokenRepo.save({
userId,
token: hashedToken,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
});
return token;
}
}
// Proper JWT strategy with validation
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private config: ConfigService,
private usersService: UsersService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: config.get<string>('JWT_SECRET'),
ignoreExpiration: false,
issuer: config.get<string>('JWT_ISSUER'),
audience: config.get<string>('JWT_AUDIENCE'),
});
}
async validate(payload: JwtPayload): Promise<User> {
// Verify user still exists and is active
const user = await this.usersService.findById(payload.sub);
if (!user || !user.isActive) {
throw new UnauthorizedException('User not found or inactive');
}
// Verify token wasn't issued before password change
if (user.passwordChangedAt) {
const tokenIssuedAt = new Date(payload.iat * 1000);
if (tokenIssuedAt < user.passwordChangedAt) {
throw new UnauthorizedException('Token invalidated by password change');
}
}
return user;
}
}
```
Reference: [NestJS Authentication](https://docs.nestjs.com/security/authentication)
@@ -0,0 +1,125 @@
---
title: Implement Rate Limiting
impact: HIGH
impactDescription: Protects against abuse and ensures fair resource usage
tags: security, rate-limiting, throttler, protection
---
## Implement Rate Limiting
Use `@nestjs/throttler` to limit request rates per client. Apply different limits for different endpoints - stricter for auth endpoints, more relaxed for read operations. Consider using Redis for distributed rate limiting in clustered deployments.
**Incorrect (no rate limiting on sensitive endpoints):**
```typescript
// No rate limiting on sensitive endpoints
@Controller('auth')
export class AuthController {
@Post('login')
async login(@Body() dto: LoginDto): Promise<TokenResponse> {
// Attackers can brute-force credentials
return this.authService.login(dto);
}
@Post('forgot-password')
async forgotPassword(@Body() dto: ForgotPasswordDto): Promise<void> {
// Can be abused to spam users with emails
return this.authService.sendResetEmail(dto.email);
}
}
// Same limits for all endpoints
@UseGuards(ThrottlerGuard)
@Controller('api')
export class ApiController {
@Get('public-data')
async getPublic() {} // Should allow more requests
@Post('process-payment')
async payment() {} // Should be more restrictive
}
```
**Correct (configured throttler with endpoint-specific limits):**
```typescript
// Configure throttler globally with multiple limits
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
@Module({
imports: [
ThrottlerModule.forRoot([
{
name: 'short',
ttl: 1000, // 1 second
limit: 3, // 3 requests per second
},
{
name: 'medium',
ttl: 10000, // 10 seconds
limit: 20, // 20 requests per 10 seconds
},
{
name: 'long',
ttl: 60000, // 1 minute
limit: 100, // 100 requests per minute
},
]),
],
providers: [
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
})
export class AppModule {}
// Override limits per endpoint
@Controller('auth')
export class AuthController {
@Post('login')
@Throttle({ short: { limit: 5, ttl: 60000 } }) // 5 attempts per minute
async login(@Body() dto: LoginDto): Promise<TokenResponse> {
return this.authService.login(dto);
}
@Post('forgot-password')
@Throttle({ short: { limit: 3, ttl: 3600000 } }) // 3 per hour
async forgotPassword(@Body() dto: ForgotPasswordDto): Promise<void> {
return this.authService.sendResetEmail(dto.email);
}
}
// Skip throttling for certain routes
@Controller('health')
export class HealthController {
@Get()
@SkipThrottle()
check(): string {
return 'OK';
}
}
// Custom throttle per user type
@Injectable()
export class CustomThrottlerGuard extends ThrottlerGuard {
protected async getTracker(req: Request): Promise<string> {
// Use user ID if authenticated, IP otherwise
return req.user?.id || req.ip;
}
protected async getLimit(context: ExecutionContext): Promise<number> {
const request = context.switchToHttp().getRequest();
// Higher limits for authenticated users
if (request.user) {
return request.user.isPremium ? 1000 : 200;
}
return 50; // Anonymous users
}
}
```
Reference: [NestJS Throttler](https://docs.nestjs.com/security/rate-limiting)
@@ -0,0 +1,139 @@
---
title: Sanitize Output to Prevent XSS
impact: HIGH
impactDescription: XSS vulnerabilities can compromise user sessions and data
tags: security, xss, sanitization, html
---
## Sanitize Output to Prevent XSS
While NestJS APIs typically return JSON (which browsers don't execute), XSS risks exist when rendering HTML, storing user content, or when frontend frameworks improperly handle API responses. Sanitize user-generated content before storage and use proper Content-Type headers.
**Incorrect (storing raw HTML without sanitization):**
```typescript
// Store raw HTML from users
@Injectable()
export class CommentsService {
async create(dto: CreateCommentDto): Promise<Comment> {
// User can inject: <script>steal(document.cookie)</script>
return this.repo.save({
content: dto.content, // Raw, unsanitized
authorId: dto.authorId,
});
}
}
// Return HTML without sanitization
@Controller('pages')
export class PagesController {
@Get(':slug')
@Header('Content-Type', 'text/html')
async getPage(@Param('slug') slug: string): Promise<string> {
const page = await this.pagesService.findBySlug(slug);
// If page.content contains user input, XSS is possible
return `<html><body>${page.content}</body></html>`;
}
}
// Reflect user input in errors
@Get(':id')
async findOne(@Param('id') id: string): Promise<User> {
const user = await this.repo.findOne({ where: { id } });
if (!user) {
// XSS if id contains malicious content and error is rendered
throw new NotFoundException(`User ${id} not found`);
}
return user;
}
```
**Correct (sanitize content and use proper headers):**
```typescript
// Sanitize HTML content before storage
import * as sanitizeHtml from 'sanitize-html';
@Injectable()
export class CommentsService {
private readonly sanitizeOptions: sanitizeHtml.IOptions = {
allowedTags: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
allowedAttributes: {
a: ['href', 'title'],
},
allowedSchemes: ['http', 'https', 'mailto'],
};
async create(dto: CreateCommentDto): Promise<Comment> {
return this.repo.save({
content: sanitizeHtml(dto.content, this.sanitizeOptions),
authorId: dto.authorId,
});
}
}
// Use validation pipe to strip HTML
import { Transform } from 'class-transformer';
export class CreatePostDto {
@IsString()
@MaxLength(1000)
@Transform(({ value }) => sanitizeHtml(value, { allowedTags: [] }))
title: string;
@IsString()
@Transform(({ value }) =>
sanitizeHtml(value, {
allowedTags: ['p', 'br', 'b', 'i', 'a'],
allowedAttributes: { a: ['href'] },
}),
)
content: string;
}
// Set proper Content-Type headers
@Controller('api')
export class ApiController {
@Get('data')
@Header('Content-Type', 'application/json')
async getData(): Promise<DataResponse> {
// JSON response - browser won't execute scripts
return this.service.getData();
}
}
// Sanitize error messages
@Get(':id')
async findOne(@Param('id', ParseUUIDPipe) id: string): Promise<User> {
const user = await this.repo.findOne({ where: { id } });
if (!user) {
// UUID validation ensures safe format
throw new NotFoundException('User not found');
}
return user;
}
// Use Helmet for CSP headers
import helmet from 'helmet';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
},
},
}),
);
await app.listen(3000);
}
```
Reference: [OWASP XSS Prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html)
@@ -0,0 +1,135 @@
---
title: Use Guards for Authentication and Authorization
impact: HIGH
impactDescription: Enforces access control before handlers execute
tags: security, guards, authentication, authorization
---
## Use Guards for Authentication and Authorization
Guards determine whether a request should be handled based on authentication state, roles, permissions, or other conditions. They run after middleware but before pipes and interceptors, making them ideal for access control. Use guards instead of manual checks in controllers.
**Incorrect (manual auth checks in every handler):**
```typescript
// Manual auth checks in every handler
@Controller('admin')
export class AdminController {
@Get('users')
async getUsers(@Request() req) {
if (!req.user) {
throw new UnauthorizedException();
}
if (!req.user.roles.includes('admin')) {
throw new ForbiddenException();
}
return this.adminService.getUsers();
}
@Delete('users/:id')
async deleteUser(@Request() req, @Param('id') id: string) {
if (!req.user) {
throw new UnauthorizedException();
}
if (!req.user.roles.includes('admin')) {
throw new ForbiddenException();
}
return this.adminService.deleteUser(id);
}
}
```
**Correct (guards with declarative decorators):**
```typescript
// JWT Auth Guard
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(
private jwtService: JwtService,
private reflector: Reflector,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
// Check for @Public() decorator
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
context.getHandler(),
context.getClass(),
]);
if (isPublic) return true;
const request = context.switchToHttp().getRequest();
const token = this.extractToken(request);
if (!token) {
throw new UnauthorizedException('No token provided');
}
try {
request.user = await this.jwtService.verifyAsync(token);
return true;
} catch {
throw new UnauthorizedException('Invalid token');
}
}
private extractToken(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
// Roles Guard
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>('roles', [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) return true;
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
// Decorators
export const Public = () => SetMetadata('isPublic', true);
export const Roles = (...roles: Role[]) => SetMetadata('roles', roles);
// Register guards globally
@Module({
providers: [
{ provide: APP_GUARD, useClass: JwtAuthGuard },
{ provide: APP_GUARD, useClass: RolesGuard },
],
})
export class AppModule {}
// Clean controller
@Controller('admin')
@Roles(Role.Admin) // Applied to all routes
export class AdminController {
@Get('users')
getUsers(): Promise<User[]> {
return this.adminService.getUsers();
}
@Delete('users/:id')
deleteUser(@Param('id') id: string): Promise<void> {
return this.adminService.deleteUser(id);
}
@Public() // Override: no auth required
@Get('health')
health() {
return { status: 'ok' };
}
}
```
Reference: [NestJS Guards](https://docs.nestjs.com/guards)
@@ -0,0 +1,150 @@
---
title: Validate All Input with DTOs and Pipes
impact: HIGH
impactDescription: First line of defense against attacks
tags: security, validation, dto, pipes
---
## Validate All Input with DTOs and Pipes
Always validate incoming data using class-validator decorators on DTOs and the global ValidationPipe. Never trust user input. Validate all request bodies, query parameters, and route parameters before processing.
**Incorrect (trust raw input without validation):**
```typescript
// Trust raw input without validation
@Controller('users')
export class UsersController {
@Post()
create(@Body() body: any) {
// body could contain anything - SQL injection, XSS, etc.
return this.usersService.create(body);
}
@Get()
findAll(@Query() query: any) {
// query.limit could be "'; DROP TABLE users; --"
return this.usersService.findAll(query.limit);
}
}
// DTOs without validation decorators
export class CreateUserDto {
name: string; // No validation
email: string; // Could be "not-an-email"
age: number; // Could be "abc" or -999
}
```
**Correct (validated DTOs with global ValidationPipe):**
```typescript
// Enable ValidationPipe globally in main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // Strip unknown properties
forbidNonWhitelisted: true, // Throw on unknown properties
transform: true, // Auto-transform to DTO types
transformOptions: {
enableImplicitConversion: true,
},
}),
);
await app.listen(3000);
}
// Create well-validated DTOs
import {
IsString,
IsEmail,
IsInt,
Min,
Max,
IsOptional,
MinLength,
MaxLength,
Matches,
IsNotEmpty,
} from 'class-validator';
import { Transform, Type } from 'class-transformer';
export class CreateUserDto {
@IsString()
@IsNotEmpty()
@MinLength(2)
@MaxLength(100)
@Transform(({ value }) => value?.trim())
name: string;
@IsEmail()
@Transform(({ value }) => value?.toLowerCase().trim())
email: string;
@IsInt()
@Min(0)
@Max(150)
age: number;
@IsString()
@MinLength(8)
@MaxLength(100)
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, {
message: 'Password must contain uppercase, lowercase, and number',
})
password: string;
}
// Query DTO with defaults and transformation
export class FindUsersQueryDto {
@IsOptional()
@IsString()
@MaxLength(100)
search?: string;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(100)
limit: number = 20;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(0)
offset: number = 0;
}
// Param validation
export class UserIdParamDto {
@IsUUID('4')
id: string;
}
@Controller('users')
export class UsersController {
@Post()
create(@Body() dto: CreateUserDto): Promise<User> {
// dto is guaranteed to be valid
return this.usersService.create(dto);
}
@Get()
findAll(@Query() query: FindUsersQueryDto): Promise<User[]> {
// query.limit is a number, query.search is sanitized
return this.usersService.findAll(query);
}
@Get(':id')
findOne(@Param() params: UserIdParamDto): Promise<User> {
// params.id is a valid UUID
return this.usersService.findById(params.id);
}
}
```
Reference: [NestJS Validation](https://docs.nestjs.com/techniques/validation)
@@ -0,0 +1,178 @@
---
title: Use Supertest for E2E Testing
impact: HIGH
impactDescription: Validates the full request/response cycle
tags: testing, e2e, supertest, integration
---
## Use Supertest for E2E Testing
End-to-end tests use Supertest to make real HTTP requests against your NestJS application. They test the full stack including middleware, guards, pipes, and interceptors. E2E tests catch integration issues that unit tests miss.
**Incorrect (no proper E2E setup or teardown):**
```typescript
// Only unit test controllers
describe('UsersController', () => {
it('should return users', async () => {
const service = { findAll: jest.fn().mockResolvedValue([]) };
const controller = new UsersController(service as any);
const result = await controller.findAll();
expect(result).toEqual([]);
// Doesn't test: routes, guards, pipes, serialization
});
});
// E2E tests without proper setup/teardown
describe('Users API', () => {
it('should create user', async () => {
const app = await NestFactory.create(AppModule);
// No proper initialization
// No cleanup after test
// Hits real database
});
});
```
**Correct (proper E2E setup with Supertest):**
```typescript
// Proper E2E test setup
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
describe('UsersController (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
// Apply same config as production
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
}),
);
await app.init();
});
afterAll(async () => {
await app.close();
});
describe('/users (POST)', () => {
it('should create a user', () => {
return request(app.getHttpServer())
.post('/users')
.send({ name: 'John', email: 'john@test.com' })
.expect(201)
.expect((res) => {
expect(res.body).toHaveProperty('id');
expect(res.body.name).toBe('John');
expect(res.body.email).toBe('john@test.com');
});
});
it('should return 400 for invalid email', () => {
return request(app.getHttpServer())
.post('/users')
.send({ name: 'John', email: 'invalid-email' })
.expect(400)
.expect((res) => {
expect(res.body.message).toContain('email');
});
});
});
describe('/users/:id (GET)', () => {
it('should return 404 for non-existent user', () => {
return request(app.getHttpServer())
.get('/users/non-existent-id')
.expect(404);
});
});
});
// Testing with authentication
describe('Protected Routes (e2e)', () => {
let app: INestApplication;
let authToken: string;
beforeAll(async () => {
const moduleFixture = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
await app.init();
// Get auth token
const loginResponse = await request(app.getHttpServer())
.post('/auth/login')
.send({ email: 'test@test.com', password: 'password' });
authToken = loginResponse.body.accessToken;
});
it('should return 401 without token', () => {
return request(app.getHttpServer())
.get('/users/me')
.expect(401);
});
it('should return user profile with valid token', () => {
return request(app.getHttpServer())
.get('/users/me')
.set('Authorization', `Bearer ${authToken}`)
.expect(200)
.expect((res) => {
expect(res.body.email).toBe('test@test.com');
});
});
});
// Database isolation for E2E tests
describe('Orders API (e2e)', () => {
let app: INestApplication;
let dataSource: DataSource;
beforeAll(async () => {
const moduleFixture = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
envFilePath: '.env.test', // Test database config
}),
AppModule,
],
}).compile();
app = moduleFixture.createNestApplication();
dataSource = moduleFixture.get(DataSource);
await app.init();
});
beforeEach(async () => {
// Clean database between tests
await dataSource.synchronize(true);
});
afterAll(async () => {
await dataSource.destroy();
await app.close();
});
});
```
Reference: [NestJS E2E Testing](https://docs.nestjs.com/fundamentals/testing#end-to-end-testing)
@@ -0,0 +1,179 @@
---
title: Mock External Services in Tests
impact: HIGH
impactDescription: Ensures fast, reliable, deterministic tests
tags: testing, mocking, external-services, jest
---
## Mock External Services in Tests
Never call real external services (APIs, databases, message queues) in unit tests. Mock them to ensure tests are fast, deterministic, and don't incur costs. Use realistic mock data and test edge cases like timeouts and errors.
**Incorrect (calling real APIs and databases):**
```typescript
// Call real APIs in tests
describe('PaymentService', () => {
it('should process payment', async () => {
const service = new PaymentService(new StripeClient(realApiKey));
// Hits real Stripe API!
const result = await service.charge('tok_visa', 1000);
// Slow, costs money, flaky
});
});
// Use real database
describe('UsersService', () => {
beforeEach(async () => {
await connection.query('DELETE FROM users'); // Modifies real DB
});
it('should create user', async () => {
const user = await service.create({ email: 'test@test.com' });
// Side effects on shared database
});
});
// Incomplete mocks
const mockHttpService = {
get: jest.fn().mockResolvedValue({ data: {} }),
// Missing error scenarios, missing other methods
};
```
**Correct (mock all external dependencies):**
```typescript
// Mock HTTP service properly
describe('WeatherService', () => {
let service: WeatherService;
let httpService: jest.Mocked<HttpService>;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
WeatherService,
{
provide: HttpService,
useValue: {
get: jest.fn(),
post: jest.fn(),
},
},
],
}).compile();
service = module.get(WeatherService);
httpService = module.get(HttpService);
});
it('should return weather data', async () => {
const mockResponse = {
data: { temperature: 72, humidity: 45 },
status: 200,
statusText: 'OK',
headers: {},
config: {},
};
httpService.get.mockReturnValue(of(mockResponse));
const result = await service.getWeather('NYC');
expect(result).toEqual({ temperature: 72, humidity: 45 });
});
it('should handle API timeout', async () => {
httpService.get.mockReturnValue(
throwError(() => new Error('ETIMEDOUT')),
);
await expect(service.getWeather('NYC')).rejects.toThrow('Weather service unavailable');
});
it('should handle rate limiting', async () => {
httpService.get.mockReturnValue(
throwError(() => ({
response: { status: 429, data: { message: 'Rate limited' } },
})),
);
await expect(service.getWeather('NYC')).rejects.toThrow(TooManyRequestsException);
});
});
// Mock repository instead of database
describe('UsersService', () => {
let service: UsersService;
let repo: jest.Mocked<Repository<User>>;
beforeEach(async () => {
const mockRepo = {
find: jest.fn(),
findOne: jest.fn(),
save: jest.fn(),
delete: jest.fn(),
createQueryBuilder: jest.fn(),
};
const module = await Test.createTestingModule({
providers: [
UsersService,
{ provide: getRepositoryToken(User), useValue: mockRepo },
],
}).compile();
service = module.get(UsersService);
repo = module.get(getRepositoryToken(User));
});
it('should find user by id', async () => {
const mockUser = { id: '1', name: 'John', email: 'john@test.com' };
repo.findOne.mockResolvedValue(mockUser);
const result = await service.findById('1');
expect(result).toEqual(mockUser);
expect(repo.findOne).toHaveBeenCalledWith({ where: { id: '1' } });
});
});
// Create mock factory for complex SDKs
function createMockStripe(): jest.Mocked<Stripe> {
return {
paymentIntents: {
create: jest.fn(),
retrieve: jest.fn(),
confirm: jest.fn(),
cancel: jest.fn(),
},
customers: {
create: jest.fn(),
retrieve: jest.fn(),
},
} as any;
}
// Mock time for time-dependent tests
describe('TokenService', () => {
beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2024-01-15'));
});
afterEach(() => {
jest.useRealTimers();
});
it('should expire token after 1 hour', async () => {
const token = await service.createToken();
// Fast-forward time
jest.advanceTimersByTime(61 * 60 * 1000);
expect(await service.isValid(token)).toBe(false);
});
});
```
Reference: [Jest Mocking](https://jestjs.io/docs/mock-functions)
@@ -0,0 +1,153 @@
---
title: Use Testing Module for Unit Tests
impact: HIGH
impactDescription: Enables proper isolated testing with mocked dependencies
tags: testing, unit-tests, mocking, jest
---
## Use Testing Module for Unit Tests
Use `@nestjs/testing` module to create isolated test environments with mocked dependencies. This ensures your tests run fast, don't depend on external services, and properly test your business logic in isolation.
**Incorrect (manual instantiation bypassing DI):**
```typescript
// Instantiate services manually without DI
describe('UsersService', () => {
it('should create user', async () => {
// Manual instantiation bypasses DI
const repo = new UserRepository(); // Real repo!
const service = new UsersService(repo);
const user = await service.create({ name: 'Test' });
// This hits the real database!
});
});
// Test implementation details
describe('UsersController', () => {
it('should call service', async () => {
const service = { create: jest.fn() };
const controller = new UsersController(service as any);
await controller.create({ name: 'Test' });
expect(service.create).toHaveBeenCalled(); // Tests implementation, not behavior
});
});
```
**Correct (use Test.createTestingModule with mocked dependencies):**
```typescript
// Use Test.createTestingModule for proper DI
import { Test, TestingModule } from '@nestjs/testing';
describe('UsersService', () => {
let service: UsersService;
let repo: jest.Mocked<UserRepository>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: UserRepository,
useValue: {
save: jest.fn(),
findOne: jest.fn(),
find: jest.fn(),
},
},
],
}).compile();
service = module.get<UsersService>(UsersService);
repo = module.get(UserRepository);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('create', () => {
it('should save and return user', async () => {
const dto = { name: 'John', email: 'john@test.com' };
const expectedUser = { id: '1', ...dto };
repo.save.mockResolvedValue(expectedUser);
const result = await service.create(dto);
expect(result).toEqual(expectedUser);
expect(repo.save).toHaveBeenCalledWith(dto);
});
it('should throw on duplicate email', async () => {
repo.findOne.mockResolvedValue({ id: '1', email: 'test@test.com' });
await expect(
service.create({ name: 'Test', email: 'test@test.com' }),
).rejects.toThrow(ConflictException);
});
});
describe('findById', () => {
it('should return user when found', async () => {
const user = { id: '1', name: 'John' };
repo.findOne.mockResolvedValue(user);
const result = await service.findById('1');
expect(result).toEqual(user);
});
it('should throw NotFoundException when not found', async () => {
repo.findOne.mockResolvedValue(null);
await expect(service.findById('999')).rejects.toThrow(NotFoundException);
});
});
});
// Testing guards and interceptors
describe('RolesGuard', () => {
let guard: RolesGuard;
let reflector: Reflector;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [RolesGuard, Reflector],
}).compile();
guard = module.get<RolesGuard>(RolesGuard);
reflector = module.get<Reflector>(Reflector);
});
it('should allow when no roles required', () => {
const context = createMockExecutionContext({ user: { roles: [] } });
jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(undefined);
expect(guard.canActivate(context)).toBe(true);
});
it('should allow admin for admin-only route', () => {
const context = createMockExecutionContext({ user: { roles: ['admin'] } });
jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(['admin']);
expect(guard.canActivate(context)).toBe(true);
});
});
function createMockExecutionContext(request: Partial<Request>): ExecutionContext {
return {
switchToHttp: () => ({
getRequest: () => request,
}),
getHandler: () => jest.fn(),
getClass: () => jest.fn(),
} as ExecutionContext;
}
```
Reference: [NestJS Testing](https://docs.nestjs.com/fundamentals/testing)
@@ -0,0 +1,212 @@
---
name: project-guidelines-example
description: NestJS modular TDD project guidelines with Drizzle and PostgreSQL. Use when scaffolding features, reviewing structure, or aligning code with this repo's conventions.
---
# Project Guidelines
Project skill for this NestJS API. Contains architecture, file structure, code patterns, testing, and deployment conventions.
## When to Use
Reference this skill when working on this project. It contains:
- Architecture overview
- File structure
- Code patterns
- Testing requirements
- Deployment workflow
---
## Architecture Overview
**Tech Stack:**
- **Backend**: NestJS
- **Database**: PostgreSQL using Drizzle ORM
- **Testing**: NestJS TestingModule (unit) and Supertest (E2E)
---
## File Structure
```
project/
├── src/
│ ├── main.ts # Application bootstrap
│ ├── app.module.ts # Root module
│ ├── common/ # Shared cross-cutting concerns
│ │ ├── decorators/
│ │ ├── filters/
│ │ ├── guards/
│ │ ├── interceptors/
│ │ └── pipes/
│ ├── config/ # Env and database config
│ └── modules/ # Feature modules (one folder per domain)
│ ├── auth/
│ │ ├── auth.module.ts
│ │ ├── auth.controller.ts
│ │ ├── auth.controller.spec.ts # Unit tests (colocated)
│ │ ├── auth.service.ts
│ │ ├── auth.service.spec.ts
│ │ ├── dto/
│ │ └── strategies/
│ └── users/
│ ├── users.module.ts
│ ├── users.controller.ts
│ ├── users.controller.spec.ts
│ ├── users.service.ts
│ ├── users.service.spec.ts
│ └── dto/
├── test/ # E2E tests (NestJS convention)
│ ├── jest-e2e.json
│ ├── app.e2e-spec.ts
│ ├── auth.e2e-spec.ts
│ └── users.e2e-spec.ts
├── drizzle/ # Schema and migrations
│ ├── schema.ts
│ └── migrations/
├── drizzle.config.ts # Drizzle Kit config
├── deploy/ # Deployment configs
├── docs/ # Documentation
└── scripts/ # Utility scripts
```
---
## Code Patterns
### Controller + DTO
```typescript
export class CreateUserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(8)
password: string;
}
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
create(@Body() dto: CreateUserDto) {
return this.usersService.create(dto);
}
}
```
### Drizzle Repository
```typescript
@Injectable()
export class UsersRepository {
constructor(@Inject(DRIZZLE) private readonly db: DrizzleDB) {}
findById(id: string) {
return this.db.query.users.findFirst({
where: eq(users.id, id),
});
}
create(data: typeof users.$inferInsert) {
return this.db.insert(users).values(data).returning();
}
}
```
---
## Testing Requirements
```bash
# Unit tests
npm test
# Coverage
npm run test:cov
# E2E
npm run test:e2e
```
**Unit test (colocated `*.spec.ts`):**
```typescript
const moduleRef = await Test.createTestingModule({
providers: [
UsersService,
{ provide: UsersRepository, useValue: { findById: jest.fn() } },
],
}).compile();
const service = moduleRef.get(UsersService);
```
**E2E test (`test/*.e2e-spec.ts`):**
```typescript
describe('UsersController (e2e)', () => {
it('/users (POST)', () => {
return request(app.getHttpServer())
.post('/users')
.send({ email: 'ada@example.com', password: 'secret123' })
.expect(201);
});
});
```
---
## Deployment Workflow
### Pre-Deployment Checklist
- [ ] All tests passing locally
- [ ] `npm run build` succeeds
- [ ] `npm run test:e2e` passes
- [ ] No hardcoded secrets
- [ ] Environment variables documented
- [ ] Drizzle migrations applied
### Deployment Commands
```bash
npm run build
npx drizzle-kit migrate
# deploy the NestJS API (Cloud Run, Fly, or similar)
```
### Environment Variables
```bash
DATABASE_URL=postgresql://user:pass@host:5432/tracking
PORT=3000
JWT_SECRET=...
```
---
## Critical Rules
1. **No emojis** in code, comments, or documentation
2. **Immutability** - never mutate objects or arrays
3. **TDD** - write tests before implementation
4. **80% coverage** minimum
5. **Many small files** - 200-400 lines typical, 800 max
6. **No console.log** in production code
7. **Exception filters** for HTTP errors, not raw try/catch in controllers
8. **Input validation** with class-validator DTOs
---
## Related Skills
- `.agents/skills/coding-standards/` - General coding best practices
- `.agents/skills/backend-patterns/` - API and database patterns
- `.agents/skills/nestjs-best-practices/` - NestJS architecture and security
- `.agents/skills/tdd-workflow/` - Test-driven development methodology
+494
View File
@@ -0,0 +1,494 @@
---
name: security-review
description: Use this skill when adding authentication, handling user input, working with secrets, creating API endpoints, or implementing payment/sensitive features. Provides comprehensive security checklist and patterns.
---
# Security Review Skill
This skill ensures all code follows security best practices and identifies potential vulnerabilities.
## When to Activate
- Implementing authentication or authorization
- Handling user input or file uploads
- Creating new API endpoints
- Working with secrets or credentials
- Implementing payment features
- Storing or transmitting sensitive data
- Integrating third-party APIs
## Security Checklist
### 1. Secrets Management
#### ❌ NEVER Do This
```typescript
const apiKey = "sk-proj-xxxxx" // Hardcoded secret
const dbPassword = "password123" // In source code
```
#### ✅ ALWAYS Do This
```typescript
const apiKey = process.env.OPENAI_API_KEY
const dbUrl = process.env.DATABASE_URL
// Verify secrets exist
if (!apiKey) {
throw new Error('OPENAI_API_KEY not configured')
}
```
#### Verification Steps
- [ ] No hardcoded API keys, tokens, or passwords
- [ ] All secrets in environment variables
- [ ] `.env.local` in .gitignore
- [ ] No secrets in git history
- [ ] Production secrets in hosting platform (Vercel, Railway)
### 2. Input Validation
#### Always Validate User Input
```typescript
import { z } from 'zod'
// Define validation schema
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
age: z.number().int().min(0).max(150)
})
// Validate before processing
export async function createUser(input: unknown) {
try {
const validated = CreateUserSchema.parse(input)
return await db.users.create(validated)
} catch (error) {
if (error instanceof z.ZodError) {
return { success: false, errors: error.errors }
}
throw error
}
}
```
#### File Upload Validation
```typescript
function validateFileUpload(file: File) {
// Size check (5MB max)
const maxSize = 5 * 1024 * 1024
if (file.size > maxSize) {
throw new Error('File too large (max 5MB)')
}
// Type check
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif']
if (!allowedTypes.includes(file.type)) {
throw new Error('Invalid file type')
}
// Extension check
const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif']
const extension = file.name.toLowerCase().match(/\.[^.]+$/)?.[0]
if (!extension || !allowedExtensions.includes(extension)) {
throw new Error('Invalid file extension')
}
return true
}
```
#### Verification Steps
- [ ] All user inputs validated with schemas
- [ ] File uploads restricted (size, type, extension)
- [ ] No direct use of user input in queries
- [ ] Whitelist validation (not blacklist)
- [ ] Error messages don't leak sensitive info
### 3. SQL Injection Prevention
#### ❌ NEVER Concatenate SQL
```typescript
// DANGEROUS - SQL Injection vulnerability
const query = `SELECT * FROM users WHERE email = '${userEmail}'`
await db.query(query)
```
#### ✅ ALWAYS Use Parameterized Queries
```typescript
// Safe - parameterized query
const { data } = await supabase
.from('users')
.select('*')
.eq('email', userEmail)
// Or with raw SQL
await db.query(
'SELECT * FROM users WHERE email = $1',
[userEmail]
)
```
#### Verification Steps
- [ ] All database queries use parameterized queries
- [ ] No string concatenation in SQL
- [ ] ORM/query builder used correctly
- [ ] Parameterized queries / Drizzle query builder only
### 4. Authentication & Authorization
#### JWT Token Handling
```typescript
// ❌ WRONG: localStorage (vulnerable to XSS)
localStorage.setItem('token', token)
// ✅ CORRECT: httpOnly cookies
res.setHeader('Set-Cookie',
`token=${token}; HttpOnly; Secure; SameSite=Strict; Max-Age=3600`)
```
#### Authorization Checks
```typescript
export async function deleteUser(userId: string, requesterId: string) {
// ALWAYS verify authorization first
const requester = await db.users.findUnique({
where: { id: requesterId }
})
if (requester.role !== 'admin') {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 403 }
)
}
// Proceed with deletion
await db.users.delete({ where: { id: userId } })
}
```
#### Database Access Control
```sql
-- Enable RLS on all tables
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
-- Users can only view their own data
CREATE POLICY "Users view own data"
ON users FOR SELECT
USING (auth.uid() = id);
-- Users can only update their own data
CREATE POLICY "Users update own data"
ON users FOR UPDATE
USING (auth.uid() = id);
```
#### Verification Steps
- [ ] Tokens stored in httpOnly cookies (not localStorage)
- [ ] Authorization checks before sensitive operations
- [ ] Least-privilege database roles; no client DB access
- [ ] Role-based access control implemented
- [ ] Session management secure
### 5. XSS Prevention
#### Sanitize HTML
```typescript
import DOMPurify from 'isomorphic-dompurify'
// ALWAYS sanitize user-provided HTML
function renderUserContent(html: string) {
const clean = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p'],
ALLOWED_ATTR: []
})
return <div dangerouslySetInnerHTML={{ __html: clean }} />
}
```
#### Content Security Policy
```typescript
// next.config.js
const securityHeaders = [
{
key: 'Content-Security-Policy',
value: `
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.example.com;
`.replace(/\s{2,}/g, ' ').trim()
}
]
```
#### Verification Steps
- [ ] User-provided HTML sanitized
- [ ] CSP headers configured
- [ ] No unvalidated dynamic content rendering
- [ ] React's built-in XSS protection used
### 6. CSRF Protection
#### CSRF Tokens
```typescript
import { csrf } from '@/lib/csrf'
export async function POST(request: Request) {
const token = request.headers.get('X-CSRF-Token')
if (!csrf.verify(token)) {
return NextResponse.json(
{ error: 'Invalid CSRF token' },
{ status: 403 }
)
}
// Process request
}
```
#### SameSite Cookies
```typescript
res.setHeader('Set-Cookie',
`session=${sessionId}; HttpOnly; Secure; SameSite=Strict`)
```
#### Verification Steps
- [ ] CSRF tokens on state-changing operations
- [ ] SameSite=Strict on all cookies
- [ ] Double-submit cookie pattern implemented
### 7. Rate Limiting
#### API Rate Limiting
```typescript
import rateLimit from 'express-rate-limit'
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
message: 'Too many requests'
})
// Apply to routes
app.use('/api/', limiter)
```
#### Expensive Operations
```typescript
// Aggressive rate limiting for searches
const searchLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 10, // 10 requests per minute
message: 'Too many search requests'
})
app.use('/api/search', searchLimiter)
```
#### Verification Steps
- [ ] Rate limiting on all API endpoints
- [ ] Stricter limits on expensive operations
- [ ] IP-based rate limiting
- [ ] User-based rate limiting (authenticated)
### 8. Sensitive Data Exposure
#### Logging
```typescript
// ❌ WRONG: Logging sensitive data
console.log('User login:', { email, password })
console.log('Payment:', { cardNumber, cvv })
// ✅ CORRECT: Redact sensitive data
console.log('User login:', { email, userId })
console.log('Payment:', { last4: card.last4, userId })
```
#### Error Messages
```typescript
// ❌ WRONG: Exposing internal details
catch (error) {
return NextResponse.json(
{ error: error.message, stack: error.stack },
{ status: 500 }
)
}
// ✅ CORRECT: Generic error messages
catch (error) {
console.error('Internal error:', error)
return NextResponse.json(
{ error: 'An error occurred. Please try again.' },
{ status: 500 }
)
}
```
#### Verification Steps
- [ ] No passwords, tokens, or secrets in logs
- [ ] Error messages generic for users
- [ ] Detailed errors only in server logs
- [ ] No stack traces exposed to users
### 9. Blockchain Security (Solana)
#### Wallet Verification
```typescript
import { verify } from '@solana/web3.js'
async function verifyWalletOwnership(
publicKey: string,
signature: string,
message: string
) {
try {
const isValid = verify(
Buffer.from(message),
Buffer.from(signature, 'base64'),
Buffer.from(publicKey, 'base64')
)
return isValid
} catch (error) {
return false
}
}
```
#### Transaction Verification
```typescript
async function verifyTransaction(transaction: Transaction) {
// Verify recipient
if (transaction.to !== expectedRecipient) {
throw new Error('Invalid recipient')
}
// Verify amount
if (transaction.amount > maxAmount) {
throw new Error('Amount exceeds limit')
}
// Verify user has sufficient balance
const balance = await getBalance(transaction.from)
if (balance < transaction.amount) {
throw new Error('Insufficient balance')
}
return true
}
```
#### Verification Steps
- [ ] Wallet signatures verified
- [ ] Transaction details validated
- [ ] Balance checks before transactions
- [ ] No blind transaction signing
### 10. Dependency Security
#### Regular Updates
```bash
# Check for vulnerabilities
npm audit
# Fix automatically fixable issues
npm audit fix
# Update dependencies
npm update
# Check for outdated packages
npm outdated
```
#### Lock Files
```bash
# ALWAYS commit lock files
git add package-lock.json
# Use in CI/CD for reproducible builds
npm ci # Instead of npm install
```
#### Verification Steps
- [ ] Dependencies up to date
- [ ] No known vulnerabilities (npm audit clean)
- [ ] Lock files committed
- [ ] Dependabot enabled on GitHub
- [ ] Regular security updates
## Security Testing
### Automated Security Tests
```typescript
// Test authentication
test('requires authentication', async () => {
const response = await fetch('/api/protected')
expect(response.status).toBe(401)
})
// Test authorization
test('requires admin role', async () => {
const response = await fetch('/api/admin', {
headers: { Authorization: `Bearer ${userToken}` }
})
expect(response.status).toBe(403)
})
// Test input validation
test('rejects invalid input', async () => {
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify({ email: 'not-an-email' })
})
expect(response.status).toBe(400)
})
// Test rate limiting
test('enforces rate limits', async () => {
const requests = Array(101).fill(null).map(() =>
fetch('/api/endpoint')
)
const responses = await Promise.all(requests)
const tooManyRequests = responses.filter(r => r.status === 429)
expect(tooManyRequests.length).toBeGreaterThan(0)
})
```
## Pre-Deployment Security Checklist
Before ANY production deployment:
- [ ] **Secrets**: No hardcoded secrets, all in env vars
- [ ] **Input Validation**: All user inputs validated
- [ ] **SQL Injection**: All queries parameterized
- [ ] **XSS**: User content sanitized
- [ ] **CSRF**: Protection enabled
- [ ] **Authentication**: Proper token handling
- [ ] **Authorization**: Role checks in place
- [ ] **Rate Limiting**: Enabled on all endpoints
- [ ] **HTTPS**: Enforced in production
- [ ] **Security Headers**: CSP, X-Frame-Options configured
- [ ] **Error Handling**: No sensitive data in errors
- [ ] **Logging**: No sensitive data logged
- [ ] **Dependencies**: Up to date, no vulnerabilities
- [ ] **Least privilege**: App uses a restricted DB role, not a superuser
- [ ] **CORS**: Properly configured
- [ ] **File Uploads**: Validated (size, type)
- [ ] **Wallet Signatures**: Verified (if blockchain)
## Resources
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
- [NestJS Security](https://docs.nestjs.com/security/authentication)
- [Drizzle ORM](https://orm.drizzle.team/docs/overview)
- [Web Security Academy](https://portswigger.net/web-security)
---
**Remember**: Security is not optional. One vulnerability can compromise the entire platform. When in doubt, err on the side of caution.
+62
View File
@@ -0,0 +1,62 @@
---
name: strategic-compact
description: Suggests manual context compaction at logical intervals to preserve context through task phases rather than arbitrary auto-compaction.
---
# Strategic Compact Skill
Suggests manual `/compact` at strategic points in your workflow rather than relying on arbitrary auto-compaction.
## Why Strategic Compaction?
Auto-compaction triggers at arbitrary points:
- Often mid-task, losing important context
- No awareness of logical task boundaries
- Can interrupt complex multi-step operations
Strategic compaction at logical boundaries:
- **After exploration, before execution** - Compact research context, keep implementation plan
- **After completing a milestone** - Fresh start for next phase
- **Before major context shifts** - Clear exploration context before different task
## How It Works
The `suggest-compact.sh` script runs on PreToolUse (Edit/Write) and:
1. **Tracks tool calls** - Counts tool invocations in session
2. **Threshold detection** - Suggests at configurable threshold (default: 50 calls)
3. **Periodic reminders** - Reminds every 25 calls after threshold
## Hook Setup
Already wired in `.cursor/hooks.json` as an `afterFileEdit` command:
```json
{
"version": 1,
"hooks": {
"afterFileEdit": [
{
"command": "node .cursor/scripts/hooks/suggest-compact.js"
}
]
}
}
```
## Configuration
Environment variables:
- `COMPACT_THRESHOLD` - Tool calls before first suggestion (default: 50)
## Best Practices
1. **Compact after planning** - Once plan is finalized, compact to start fresh
2. **Compact after debugging** - Clear error-resolution context before continuing
3. **Don't compact mid-implementation** - Preserve context for related changes
4. **Read the suggestion** - The hook tells you *when*, you decide *if*
## Related
- [The Longform Guide](https://x.com/affaanmustafa/status/2014040193557471352) - Token optimization section
- Memory persistence hooks - For state that survives compaction
+43
View File
@@ -0,0 +1,43 @@
#!/bin/bash
# Strategic Compact Suggester
# Runs on PreToolUse or periodically to suggest manual compaction at logical intervals
#
# Why manual over auto-compact:
# - Auto-compact happens at arbitrary points, often mid-task
# - Strategic compacting preserves context through logical phases
# - Compact after exploration, before execution
# - Compact after completing a milestone, before starting next
#
# Configured in .cursor/hooks.json:
# afterFileEdit -> node .cursor/scripts/hooks/suggest-compact.js
# This shell version is a fallback.
#
# Criteria for suggesting compact:
# - Session has been running for extended period
# - Large number of tool calls made
# - Transitioning from research/exploration to implementation
# - Plan has been finalized
# Track tool call count (increment in a temp file)
COUNTER_FILE="/tmp/claude-tool-count-$$"
THRESHOLD=${COMPACT_THRESHOLD:-50}
# Initialize or increment counter
if [ -f "$COUNTER_FILE" ]; then
count=$(cat "$COUNTER_FILE")
count=$((count + 1))
echo "$count" > "$COUNTER_FILE"
else
echo "1" > "$COUNTER_FILE"
count=1
fi
# Suggest compact after threshold tool calls
if [ "$count" -eq "$THRESHOLD" ]; then
echo "[StrategicCompact] $THRESHOLD tool calls reached - consider /compact if transitioning phases" >&2
fi
# Suggest at regular intervals after threshold
if [ "$count" -gt "$THRESHOLD" ] && [ $((count % 25)) -eq 0 ]; then
echo "[StrategicCompact] $count tool calls - good checkpoint for /compact if context is stale" >&2
fi
+381
View File
@@ -0,0 +1,381 @@
---
name: tdd-workflow
description: Use this skill when writing new features, fixing bugs, or refactoring code. Enforces test-driven development with 80%+ coverage including unit, integration, and E2E tests.
---
# Test-Driven Development Workflow
This skill ensures all code development follows TDD principles with comprehensive test coverage.
## When to Activate
- Writing new features or functionality
- Fixing bugs or issues
- Refactoring existing code
- Adding API endpoints
- Creating new components
## Core Principles
### 1. Tests BEFORE Code
ALWAYS write tests first, then implement code to make tests pass.
### 2. Coverage Requirements
- Minimum 80% coverage (unit + integration + E2E)
- All edge cases covered
- Error scenarios tested
- Boundary conditions verified
### 3. Test Types
#### Unit Tests
- Individual functions and utilities
- Component logic
- Pure functions
- Helpers and utilities
#### Integration Tests
- API endpoints
- Database operations
- Service interactions
- External API calls
#### E2E Tests (Supertest)
- Critical API flows
- Complete HTTP workflows
- Auth, validation, and error responses
## TDD Workflow Steps
### Step 1: Write User Journeys
```
As a [role], I want to [action], so that [benefit]
Example:
As a user, I want to search for markets semantically,
so that I can find relevant markets even without exact keywords.
```
### Step 2: Generate Test Cases
For each user journey, create comprehensive test cases:
```typescript
describe('Semantic Search', () => {
it('returns relevant markets for query', async () => {
// Test implementation
})
it('handles empty query gracefully', async () => {
// Test edge case
})
it('falls back to substring search when Redis unavailable', async () => {
// Test fallback behavior
})
it('sorts results by similarity score', async () => {
// Test sorting logic
})
})
```
### Step 3: Run Tests (They Should Fail)
```bash
npm test
# Tests should fail - we haven't implemented yet
```
### Step 4: Implement Code
Write minimal code to make tests pass:
```typescript
// Implementation guided by tests
export async function searchMarkets(query: string) {
// Implementation here
}
```
### Step 5: Run Tests Again
```bash
npm test
# Tests should now pass
```
### Step 6: Refactor
Improve code quality while keeping tests green:
- Remove duplication
- Improve naming
- Optimize performance
- Enhance readability
### Step 7: Verify Coverage
```bash
npm run test:coverage
# Verify 80%+ coverage achieved
```
## Testing Patterns
### Unit Test Pattern (Jest/Vitest)
```typescript
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from './Button'
describe('Button Component', () => {
it('renders with correct text', () => {
render(<Button>Click me</Button>)
expect(screen.getByText('Click me')).toBeInTheDocument()
})
it('calls onClick when clicked', () => {
const handleClick = jest.fn()
render(<Button onClick={handleClick}>Click</Button>)
fireEvent.click(screen.getByRole('button'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('is disabled when disabled prop is true', () => {
render(<Button disabled>Click</Button>)
expect(screen.getByRole('button')).toBeDisabled()
})
})
```
### API Integration Test Pattern
```typescript
import { NextRequest } from 'next/server'
import { GET } from './route'
describe('GET /api/markets', () => {
it('returns markets successfully', async () => {
const request = new NextRequest('http://localhost/api/markets')
const response = await GET(request)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(Array.isArray(data.data)).toBe(true)
})
it('validates query parameters', async () => {
const request = new NextRequest('http://localhost/api/markets?limit=invalid')
const response = await GET(request)
expect(response.status).toBe(400)
})
it('handles database errors gracefully', async () => {
// Mock database failure
const request = new NextRequest('http://localhost/api/markets')
// Test error handling
})
})
```
### E2E Test Pattern (Supertest)
```typescript
import { INestApplication } from '@nestjs/common'
import { Test } from '@nestjs/testing'
import request from 'supertest'
import { AppModule } from '../src/app.module'
describe('Users (e2e)', () => {
let app: INestApplication
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile()
app = moduleRef.createNestApplication()
await app.init()
})
afterAll(async () => {
await app.close()
})
it('POST /users creates a user', async () => {
await request(app.getHttpServer())
.post('/users')
.send({ email: 'ada@example.com', password: 'secret123' })
.expect(201)
})
})
```
## Test File Organization
```
src/
└── modules/
└── users/
├── users.module.ts
├── users.controller.ts
├── users.controller.spec.ts # Unit tests
├── users.service.ts
├── users.service.spec.ts
└── dto/
test/
├── jest-e2e.json
├── app.e2e-spec.ts # E2E tests
└── users.e2e-spec.ts
```
## Mocking External Services
### Drizzle Mock
```typescript
const db = {
query: {
users: {
findFirst: jest.fn(),
findMany: jest.fn(),
},
},
insert: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
}
```
### Redis Mock
```typescript
jest.mock('@/lib/redis', () => ({
searchMarketsByVector: jest.fn(() => Promise.resolve([
{ slug: 'test-market', similarity_score: 0.95 }
])),
checkRedisHealth: jest.fn(() => Promise.resolve({ connected: true }))
}))
```
### OpenAI Mock
```typescript
jest.mock('@/lib/openai', () => ({
generateEmbedding: jest.fn(() => Promise.resolve(
new Array(1536).fill(0.1) // Mock 1536-dim embedding
))
}))
```
## Test Coverage Verification
### Run Coverage Report
```bash
npm run test:coverage
```
### Coverage Thresholds
```json
{
"jest": {
"coverageThresholds": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
}
}
}
```
## Common Testing Mistakes to Avoid
### ❌ WRONG: Testing Implementation Details
```typescript
// Don't test internal state
expect(component.state.count).toBe(5)
```
### ✅ CORRECT: Test User-Visible Behavior
```typescript
// Test what users see
expect(screen.getByText('Count: 5')).toBeInTheDocument()
```
### ❌ WRONG: Brittle Selectors
```typescript
// Breaks easily
await page.click('.css-class-xyz')
```
### ✅ CORRECT: Semantic Selectors
```typescript
// Resilient to changes
await page.click('button:has-text("Submit")')
await page.click('[data-testid="submit-button"]')
```
### ❌ WRONG: No Test Isolation
```typescript
// Tests depend on each other
test('creates user', () => { /* ... */ })
test('updates same user', () => { /* depends on previous test */ })
```
### ✅ CORRECT: Independent Tests
```typescript
// Each test sets up its own data
test('creates user', () => {
const user = createTestUser()
// Test logic
})
test('updates user', () => {
const user = createTestUser()
// Update logic
})
```
## Continuous Testing
### Watch Mode During Development
```bash
npm test -- --watch
# Tests run automatically on file changes
```
### Pre-Commit Hook
```bash
# Runs before every commit
npm test && npm run lint
```
### CI/CD Integration
```yaml
# GitHub Actions
- name: Run Tests
run: npm test -- --coverage
- name: Upload Coverage
uses: codecov/codecov-action@v3
```
## Best Practices
1. **Write Tests First** - Always TDD
2. **One Assert Per Test** - Focus on single behavior
3. **Descriptive Test Names** - Explain what's tested
4. **Arrange-Act-Assert** - Clear test structure
5. **Mock External Dependencies** - Isolate unit tests
6. **Test Edge Cases** - Null, undefined, empty, large
7. **Test Error Paths** - Not just happy paths
8. **Keep Tests Fast** - Unit tests < 50ms each
9. **Clean Up After Tests** - No side effects
10. **Review Coverage Reports** - Identify gaps
## Success Metrics
- 80%+ code coverage achieved
- All tests passing (green)
- No skipped or disabled tests
- Fast test execution (< 30s for unit tests)
- E2E tests cover critical user flows
- Tests catch bugs before production
---
**Remember**: Tests are not optional. They are the safety net that enables confident refactoring, rapid development, and production reliability.
+120
View File
@@ -0,0 +1,120 @@
# Verification Loop Skill
A comprehensive verification system for Claude Code sessions.
## When to Use
Invoke this skill:
- After completing a feature or significant code change
- Before creating a PR
- When you want to ensure quality gates pass
- After refactoring
## Verification Phases
### Phase 1: Build Verification
```bash
# Check if project builds
npm run build 2>&1 | tail -20
# OR
pnpm build 2>&1 | tail -20
```
If build fails, STOP and fix before continuing.
### Phase 2: Type Check
```bash
# TypeScript projects
npx tsc --noEmit 2>&1 | head -30
# Python projects
pyright . 2>&1 | head -30
```
Report all type errors. Fix critical ones before continuing.
### Phase 3: Lint Check
```bash
# JavaScript/TypeScript
npm run lint 2>&1 | head -30
# Python
ruff check . 2>&1 | head -30
```
### Phase 4: Test Suite
```bash
# Run tests with coverage
npm run test -- --coverage 2>&1 | tail -50
# Check coverage threshold
# Target: 80% minimum
```
Report:
- Total tests: X
- Passed: X
- Failed: X
- Coverage: X%
### Phase 5: Security Scan
```bash
# Check for secrets
grep -rn "sk-" --include="*.ts" --include="*.js" . 2>/dev/null | head -10
grep -rn "api_key" --include="*.ts" --include="*.js" . 2>/dev/null | head -10
# Check for console.log
grep -rn "console.log" --include="*.ts" --include="*.tsx" src/ 2>/dev/null | head -10
```
### Phase 6: Diff Review
```bash
# Show what changed
git diff --stat
git diff HEAD~1 --name-only
```
Review each changed file for:
- Unintended changes
- Missing error handling
- Potential edge cases
## Output Format
After running all phases, produce a verification report:
```
VERIFICATION REPORT
==================
Build: [PASS/FAIL]
Types: [PASS/FAIL] (X errors)
Lint: [PASS/FAIL] (X warnings)
Tests: [PASS/FAIL] (X/Y passed, Z% coverage)
Security: [PASS/FAIL] (X issues)
Diff: [X files changed]
Overall: [READY/NOT READY] for PR
Issues to Fix:
1. ...
2. ...
```
## Continuous Mode
For long sessions, run verification every 15 minutes or after major changes:
```markdown
Set a mental checkpoint:
- After completing each function
- After finishing a component
- Before moving to next task
Run: /verify
```
## Integration with Hooks
This skill complements PostToolUse hooks but provides deeper verification.
Hooks catch issues immediately; this skill provides comprehensive review.
+209
View File
@@ -0,0 +1,209 @@
---
name: architect
description: Software architecture specialist for system design, scalability, and technical decision-making. Use PROACTIVELY when planning new features, refactoring large systems, or making architectural decisions.
tools: Read, Grep, Glob
model: opus
---
You are a senior software architect specializing in scalable, maintainable system design.
## Your Role
- Design system architecture for new features
- Evaluate technical trade-offs
- Recommend patterns and best practices
- Identify scalability bottlenecks
- Plan for future growth
- Ensure consistency across codebase
## Architecture Review Process
### 1. Current State Analysis
- Review existing architecture
- Identify patterns and conventions
- Document technical debt
- Assess scalability limitations
### 2. Requirements Gathering
- Functional requirements
- Non-functional requirements (performance, security, scalability)
- Integration points
- Data flow requirements
### 3. Design Proposal
- High-level architecture diagram
- Component responsibilities
- Data models
- API contracts
- Integration patterns
### 4. Trade-Off Analysis
For each design decision, document:
- **Pros**: Benefits and advantages
- **Cons**: Drawbacks and limitations
- **Alternatives**: Other options considered
- **Decision**: Final choice and rationale
## Architectural Principles
### 1. Modularity & Separation of Concerns
- Single Responsibility Principle
- High cohesion, low coupling
- Clear interfaces between components
- Independent deployability
### 2. Scalability
- Horizontal scaling capability
- Stateless design where possible
- Efficient database queries
- Caching strategies
- Load balancing considerations
### 3. Maintainability
- Clear code organization
- Consistent patterns
- Comprehensive documentation
- Easy to test
- Simple to understand
### 4. Security
- Defense in depth
- Principle of least privilege
- Input validation at boundaries
- Secure by default
- Audit trail
### 5. Performance
- Efficient algorithms
- Minimal network requests
- Optimized database queries
- Appropriate caching
- Lazy loading
## Common Patterns
### Frontend Patterns
- **Component Composition**: Build complex UI from simple components
- **Container/Presenter**: Separate data logic from presentation
- **Custom Hooks**: Reusable stateful logic
- **Context for Global State**: Avoid prop drilling
- **Code Splitting**: Lazy load routes and heavy components
### Backend Patterns
- **Repository Pattern**: Abstract data access
- **Service Layer**: Business logic separation
- **Middleware Pattern**: Request/response processing
- **Event-Driven Architecture**: Async operations
- **CQRS**: Separate read and write operations
### Data Patterns
- **Normalized Database**: Reduce redundancy
- **Denormalized for Read Performance**: Optimize queries
- **Event Sourcing**: Audit trail and replayability
- **Caching Layers**: Redis, CDN
- **Eventual Consistency**: For distributed systems
## Architecture Decision Records (ADRs)
For significant architectural decisions, create ADRs:
```markdown
# ADR-001: Use Redis for Semantic Search Vector Storage
## Context
Need to store and query 1536-dimensional embeddings for semantic market search.
## Decision
Use Redis Stack with vector search capability.
## Consequences
### Positive
- Fast vector similarity search (<10ms)
- Built-in KNN algorithm
- Simple deployment
- Good performance up to 100K vectors
### Negative
- In-memory storage (expensive for large datasets)
- Single point of failure without clustering
- Limited to cosine similarity
### Alternatives Considered
- **PostgreSQL pgvector**: Slower, but persistent storage
- **Pinecone**: Managed service, higher cost
- **Weaviate**: More features, more complex setup
## Status
Accepted
## Date
2025-01-15
```
## System Design Checklist
When designing a new system or feature:
### Functional Requirements
- [ ] User stories documented
- [ ] API contracts defined
- [ ] Data models specified
- [ ] UI/UX flows mapped
### Non-Functional Requirements
- [ ] Performance targets defined (latency, throughput)
- [ ] Scalability requirements specified
- [ ] Security requirements identified
- [ ] Availability targets set (uptime %)
### Technical Design
- [ ] Architecture diagram created
- [ ] Component responsibilities defined
- [ ] Data flow documented
- [ ] Integration points identified
- [ ] Error handling strategy defined
- [ ] Testing strategy planned
### Operations
- [ ] Deployment strategy defined
- [ ] Monitoring and alerting planned
- [ ] Backup and recovery strategy
- [ ] Rollback plan documented
## Red Flags
Watch for these architectural anti-patterns:
- **Big Ball of Mud**: No clear structure
- **Golden Hammer**: Using same solution for everything
- **Premature Optimization**: Optimizing too early
- **Not Invented Here**: Rejecting existing solutions
- **Analysis Paralysis**: Over-planning, under-building
- **Magic**: Unclear, undocumented behavior
- **Tight Coupling**: Components too dependent
- **God Object**: One class/component does everything
## Project-Specific Architecture (Example)
Example architecture for an AI-powered SaaS platform:
### Current Architecture
- **Backend**: NestJS (Cloud Run / Fly / similar)
- **Database**: PostgreSQL with Drizzle ORM
- **Cache**: Redis
- **Auth**: JWT with NestJS guards
### Key Design Decisions
1. **Feature modules**: One folder per domain under `src/modules/`
2. **Drizzle migrations**: Schema changes are versioned SQL, never ad-hoc
3. **TDD**: Colocated `*.spec.ts` unit tests and `test/*.e2e-spec.ts`
4. **Immutable Patterns**: Spread operators for predictable state
5. **Many Small Files**: High cohesion, low coupling
### Scalability Plan
- **10K users**: Current architecture sufficient
- **100K users**: Add Redis clustering, CDN for static assets
- **1M users**: Microservices architecture, separate read/write databases
- **10M users**: Event-driven architecture, distributed caching, multi-region
**Remember**: Good architecture enables rapid development, easy maintenance, and confident scaling. The best architecture is simple, clear, and follows established patterns.
+529
View File
@@ -0,0 +1,529 @@
---
name: build-error-resolver
description: Build and TypeScript error resolution specialist. Use PROACTIVELY when build fails or type errors occur. Fixes build/type errors only with minimal diffs, no architectural edits. Focuses on getting the build green quickly.
tools: Read, Write, Edit, Bash, Grep, Glob
model: opus
---
# Build Error Resolver
You are an expert build error resolution specialist focused on fixing TypeScript, compilation, and build errors quickly and efficiently. Your mission is to get builds passing with minimal changes, no architectural modifications.
## Core Responsibilities
1. **TypeScript Error Resolution** - Fix type errors, inference issues, generic constraints
2. **Build Error Fixing** - Resolve compilation failures, module resolution
3. **Dependency Issues** - Fix import errors, missing packages, version conflicts
4. **Configuration Errors** - Resolve tsconfig.json and NestJS config issues
5. **Minimal Diffs** - Make smallest possible changes to fix errors
6. **No Architecture Changes** - Only fix errors, don't refactor or redesign
## Tools at Your Disposal
### Build & Type Checking Tools
- **tsc** - TypeScript compiler for type checking
- **npm/yarn** - Package management
- **eslint** - Linting (can cause build failures)
- **nest build** - NestJS production build
### Diagnostic Commands
```bash
# TypeScript type check (no emit)
npx tsc --noEmit
# TypeScript with pretty output
npx tsc --noEmit --pretty
# Show all errors (don't stop at first)
npx tsc --noEmit --pretty --incremental false
# Check specific file
npx tsc --noEmit path/to/file.ts
# ESLint check
npx eslint . --ext .ts,.tsx,.js,.jsx
# NestJS build (production)
npm run build
```
## Error Resolution Workflow
### 1. Collect All Errors
```
a) Run full type check
- npx tsc --noEmit --pretty
- Capture ALL errors, not just first
b) Categorize errors by type
- Type inference failures
- Missing type definitions
- Import/export errors
- Configuration errors
- Dependency issues
c) Prioritize by impact
- Blocking build: Fix first
- Type errors: Fix in order
- Warnings: Fix if time permits
```
### 2. Fix Strategy (Minimal Changes)
```
For each error:
1. Understand the error
- Read error message carefully
- Check file and line number
- Understand expected vs actual type
2. Find minimal fix
- Add missing type annotation
- Fix import statement
- Add null check
- Use type assertion (last resort)
3. Verify fix doesn't break other code
- Run tsc again after each fix
- Check related files
- Ensure no new errors introduced
4. Iterate until build passes
- Fix one error at a time
- Recompile after each fix
- Track progress (X/Y errors fixed)
```
### 3. Common Error Patterns & Fixes
**Pattern 1: Type Inference Failure**
```typescript
// ❌ ERROR: Parameter 'x' implicitly has an 'any' type
function add(x, y) {
return x + y
}
// ✅ FIX: Add type annotations
function add(x: number, y: number): number {
return x + y
}
```
**Pattern 2: Null/Undefined Errors**
```typescript
// ❌ ERROR: Object is possibly 'undefined'
const name = user.name.toUpperCase()
// ✅ FIX: Optional chaining
const name = user?.name?.toUpperCase()
// ✅ OR: Null check
const name = user && user.name ? user.name.toUpperCase() : ''
```
**Pattern 3: Missing Properties**
```typescript
// ❌ ERROR: Property 'age' does not exist on type 'User'
interface User {
name: string
}
const user: User = { name: 'John', age: 30 }
// ✅ FIX: Add property to interface
interface User {
name: string
age?: number // Optional if not always present
}
```
**Pattern 4: Import Errors**
```typescript
// ❌ ERROR: Cannot find module '@/lib/utils'
import { formatDate } from '@/lib/utils'
// ✅ FIX 1: Check tsconfig paths are correct
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
// ✅ FIX 2: Use relative import
import { formatDate } from '../lib/utils'
// ✅ FIX 3: Install missing package
npm install @/lib/utils
```
**Pattern 5: Type Mismatch**
```typescript
// ❌ ERROR: Type 'string' is not assignable to type 'number'
const age: number = "30"
// ✅ FIX: Parse string to number
const age: number = parseInt("30", 10)
// ✅ OR: Change type
const age: string = "30"
```
**Pattern 6: Generic Constraints**
```typescript
// ❌ ERROR: Type 'T' is not assignable to type 'string'
function getLength<T>(item: T): number {
return item.length
}
// ✅ FIX: Add constraint
function getLength<T extends { length: number }>(item: T): number {
return item.length
}
// ✅ OR: More specific constraint
function getLength<T extends string | any[]>(item: T): number {
return item.length
}
```
**Pattern 7: React Hook Errors**
```typescript
// ❌ ERROR: React Hook "useState" cannot be called in a function
function MyComponent() {
if (condition) {
const [state, setState] = useState(0) // ERROR!
}
}
// ✅ FIX: Move hooks to top level
function MyComponent() {
const [state, setState] = useState(0)
if (!condition) {
return null
}
// Use state here
}
```
**Pattern 8: Async/Await Errors**
```typescript
// ❌ ERROR: 'await' expressions are only allowed within async functions
function fetchData() {
const data = await fetch('/api/data')
}
// ✅ FIX: Add async keyword
async function fetchData() {
const data = await fetch('/api/data')
}
```
**Pattern 9: Module Not Found**
```typescript
// ❌ ERROR: Cannot find module 'react' or its corresponding type declarations
import React from 'react'
// ✅ FIX: Install dependencies
npm install react
npm install --save-dev @types/react
// ✅ CHECK: Verify package.json has dependency
{
"dependencies": {
"react": "^19.0.0"
},
"devDependencies": {
"@types/react": "^19.0.0"
}
}
```
**Pattern 10: Next.js Specific Errors**
```typescript
// ❌ ERROR: Fast Refresh had to perform a full reload
// Usually caused by exporting non-component
// ✅ FIX: Separate exports
// ❌ WRONG: file.tsx
export const MyComponent = () => <div />
export const someConstant = 42 // Causes full reload
// ✅ CORRECT: component.tsx
export const MyComponent = () => <div />
// ✅ CORRECT: constants.ts
export const someConstant = 42
```
## Example Project-Specific Build Issues
### Next.js 15 + React 19 Compatibility
```typescript
// ❌ ERROR: React 19 type changes
import { FC } from 'react'
interface Props {
children: React.ReactNode
}
const Component: FC<Props> = ({ children }) => {
return <div>{children}</div>
}
// ✅ FIX: React 19 doesn't need FC
interface Props {
children: React.ReactNode
}
const Component = ({ children }: Props) => {
return <div>{children}</div>
}
```
### Supabase Client Types
```typescript
// ❌ ERROR: Type 'any' not assignable
const { data } = await supabase
.from('markets')
.select('*')
// ✅ FIX: Add type annotation
interface Market {
id: string
name: string
slug: string
// ... other fields
}
const { data } = await supabase
.from('markets')
.select('*') as { data: Market[] | null, error: any }
```
### Redis Stack Types
```typescript
// ❌ ERROR: Property 'ft' does not exist on type 'RedisClientType'
const results = await client.ft.search('idx:markets', query)
// ✅ FIX: Use proper Redis Stack types
import { createClient } from 'redis'
const client = createClient({
url: process.env.REDIS_URL
})
await client.connect()
// Type is inferred correctly now
const results = await client.ft.search('idx:markets', query)
```
### Solana Web3.js Types
```typescript
// ❌ ERROR: Argument of type 'string' not assignable to 'PublicKey'
const publicKey = wallet.address
// ✅ FIX: Use PublicKey constructor
import { PublicKey } from '@solana/web3.js'
const publicKey = new PublicKey(wallet.address)
```
## Minimal Diff Strategy
**CRITICAL: Make smallest possible changes**
### DO:
✅ Add type annotations where missing
✅ Add null checks where needed
✅ Fix imports/exports
✅ Add missing dependencies
✅ Update type definitions
✅ Fix configuration files
### DON'T:
❌ Refactor unrelated code
❌ Change architecture
❌ Rename variables/functions (unless causing error)
❌ Add new features
❌ Change logic flow (unless fixing error)
❌ Optimize performance
❌ Improve code style
**Example of Minimal Diff:**
```typescript
// File has 200 lines, error on line 45
// ❌ WRONG: Refactor entire file
// - Rename variables
// - Extract functions
// - Change patterns
// Result: 50 lines changed
// ✅ CORRECT: Fix only the error
// - Add type annotation on line 45
// Result: 1 line changed
function processData(data) { // Line 45 - ERROR: 'data' implicitly has 'any' type
return data.map(item => item.value)
}
// ✅ MINIMAL FIX:
function processData(data: any[]) { // Only change this line
return data.map(item => item.value)
}
// ✅ BETTER MINIMAL FIX (if type known):
function processData(data: Array<{ value: number }>) {
return data.map(item => item.value)
}
```
## Build Error Report Format
```markdown
# Build Error Resolution Report
**Date:** YYYY-MM-DD
**Build Target:** Next.js Production / TypeScript Check / ESLint
**Initial Errors:** X
**Errors Fixed:** Y
**Build Status:** ✅ PASSING / ❌ FAILING
## Errors Fixed
### 1. [Error Category - e.g., Type Inference]
**Location:** `src/components/MarketCard.tsx:45`
**Error Message:**
```
Parameter 'market' implicitly has an 'any' type.
```
**Root Cause:** Missing type annotation for function parameter
**Fix Applied:**
```diff
- function formatMarket(market) {
+ function formatMarket(market: Market) {
return market.name
}
```
**Lines Changed:** 1
**Impact:** NONE - Type safety improvement only
---
### 2. [Next Error Category]
[Same format]
---
## Verification Steps
1. ✅ TypeScript check passes: `npx tsc --noEmit`
2. ✅ Next.js build succeeds: `npm run build`
3. ✅ ESLint check passes: `npx eslint .`
4. ✅ No new errors introduced
5. ✅ Development server runs: `npm run dev`
## Summary
- Total errors resolved: X
- Total lines changed: Y
- Build status: ✅ PASSING
- Time to fix: Z minutes
- Blocking issues: 0 remaining
## Next Steps
- [ ] Run full test suite
- [ ] Verify in production build
- [ ] Deploy to staging for QA
```
## When to Use This Agent
**USE when:**
- `npm run build` fails
- `npx tsc --noEmit` shows errors
- Type errors blocking development
- Import/module resolution errors
- Configuration errors
- Dependency version conflicts
**DON'T USE when:**
- Code needs refactoring (use refactor-cleaner)
- Architectural changes needed (use architect)
- New features required (use planner)
- Tests failing (use tdd-guide)
- Security issues found (use security-reviewer)
## Build Error Priority Levels
### 🔴 CRITICAL (Fix Immediately)
- Build completely broken
- No development server
- Production deployment blocked
- Multiple files failing
### 🟡 HIGH (Fix Soon)
- Single file failing
- Type errors in new code
- Import errors
- Non-critical build warnings
### 🟢 MEDIUM (Fix When Possible)
- Linter warnings
- Deprecated API usage
- Non-strict type issues
- Minor configuration warnings
## Quick Reference Commands
```bash
# Check for errors
npx tsc --noEmit
# Build Next.js
npm run build
# Clear cache and rebuild
rm -rf .next node_modules/.cache
npm run build
# Check specific file
npx tsc --noEmit src/path/to/file.ts
# Install missing dependencies
npm install
# Fix ESLint issues automatically
npx eslint . --fix
# Update TypeScript
npm install --save-dev typescript@latest
# Verify node_modules
rm -rf node_modules package-lock.json
npm install
```
## Success Metrics
After build error resolution:
-`npx tsc --noEmit` exits with code 0
-`npm run build` completes successfully
- ✅ No new errors introduced
- ✅ Minimal lines changed (< 5% of affected file)
- ✅ Build time not significantly increased
- ✅ Development server runs without errors
- ✅ Tests still passing
---
**Remember**: The goal is to fix errors quickly with minimal changes. Don't refactor, don't optimize, don't redesign. Fix the error, verify the build passes, move on. Speed and precision over perfection.
+104
View File
@@ -0,0 +1,104 @@
---
name: code-reviewer
description: Expert code review specialist. Proactively reviews code for quality, security, and maintainability. Use immediately after writing or modifying code. MUST BE USED for all code changes.
tools: Read, Grep, Glob, Bash
model: opus
---
You are a senior code reviewer ensuring high standards of code quality and security.
When invoked:
1. Run git diff to see recent changes
2. Focus on modified files
3. Begin review immediately
Review checklist:
- Code is simple and readable
- Functions and variables are well-named
- No duplicated code
- Proper error handling
- No exposed secrets or API keys
- Input validation implemented
- Good test coverage
- Performance considerations addressed
- Time complexity of algorithms analyzed
- Licenses of integrated libraries checked
Provide feedback organized by priority:
- Critical issues (must fix)
- Warnings (should fix)
- Suggestions (consider improving)
Include specific examples of how to fix issues.
## Security Checks (CRITICAL)
- Hardcoded credentials (API keys, passwords, tokens)
- SQL injection risks (string concatenation in queries)
- XSS vulnerabilities (unescaped user input)
- Missing input validation
- Insecure dependencies (outdated, vulnerable)
- Path traversal risks (user-controlled file paths)
- CSRF vulnerabilities
- Authentication bypasses
## Code Quality (HIGH)
- Large functions (>50 lines)
- Large files (>800 lines)
- Deep nesting (>4 levels)
- Missing error handling (try/catch)
- console.log statements
- Mutation patterns
- Missing tests for new code
## Performance (MEDIUM)
- Inefficient algorithms (O(n²) when O(n log n) possible)
- Unnecessary re-renders in React
- Missing memoization
- Large bundle sizes
- Unoptimized images
- Missing caching
- N+1 queries
## Best Practices (MEDIUM)
- Emoji usage in code/comments
- TODO/FIXME without tickets
- Missing JSDoc for public APIs
- Accessibility issues (missing ARIA labels, poor contrast)
- Poor variable naming (x, tmp, data)
- Magic numbers without explanation
- Inconsistent formatting
## Review Output Format
For each issue:
```
[CRITICAL] Hardcoded API key
File: src/api/client.ts:42
Issue: API key exposed in source code
Fix: Move to environment variable
const apiKey = "sk-abc123"; // ❌ Bad
const apiKey = process.env.API_KEY; // ✓ Good
```
## Approval Criteria
- ✅ Approve: No CRITICAL or HIGH issues
- ⚠️ Warning: MEDIUM issues only (can merge with caution)
- ❌ Block: CRITICAL or HIGH issues found
## Project-Specific Guidelines (Example)
Add your project-specific checks here. Examples:
- Follow MANY SMALL FILES principle (200-400 lines typical)
- No emojis in codebase
- Use immutability patterns (spread operator)
- Verify database RLS policies
- Check AI integration error handling
- Validate cache fallback behavior
Customize based on your project's `CLAUDE.md` or skill files.
+443
View File
@@ -0,0 +1,443 @@
---
name: doc-updater
description: Documentation and codemap specialist. Use PROACTIVELY for updating codemaps and documentation. Runs /update-codemaps and /update-docs, generates docs/CODEMAPS/*, updates READMEs and guides.
tools: Read, Write, Edit, Bash, Grep, Glob
model: opus
---
# Documentation & Codemap Specialist
You are a documentation specialist focused on keeping codemaps and documentation current with the codebase. Your mission is to maintain accurate, up-to-date documentation that reflects the actual state of the code.
## Core Responsibilities
1. **Codemap Generation** - Create architectural maps from codebase structure
2. **Documentation Updates** - Refresh READMEs and guides from code
3. **AST Analysis** - Use TypeScript compiler API to understand structure
4. **Dependency Mapping** - Track imports/exports across modules
5. **Documentation Quality** - Ensure docs match reality
## Tools at Your Disposal
### Analysis Tools
- **ts-morph** - TypeScript AST analysis and manipulation
- **TypeScript Compiler API** - Deep code structure analysis
- **madge** - Dependency graph visualization
- **jsdoc-to-markdown** - Generate docs from JSDoc comments
### Analysis Commands
```bash
# Analyze TypeScript project structure
npx ts-morph
# Generate dependency graph
npx madge --image graph.svg src/
# Extract JSDoc comments
npx jsdoc2md src/**/*.ts
```
## Codemap Generation Workflow
### 1. Repository Structure Analysis
```
a) Identify all workspaces/packages
b) Map directory structure
c) Find entry points (apps/*, packages/*, services/*)
d) Detect framework patterns (NestJS, Node.js, etc.)
```
### 2. Module Analysis
```
For each module:
- Extract exports (public API)
- Map imports (dependencies)
- Identify routes (API routes, pages)
- Find database models (Drizzle schema)
- Locate queue/worker modules
```
### 3. Generate Codemaps
```
Structure:
docs/CODEMAPS/
├── INDEX.md # Overview of all areas
├── frontend.md # Frontend structure
├── backend.md # Backend/API structure
├── database.md # Database schema
├── integrations.md # External services
└── workers.md # Background jobs
```
### 4. Codemap Format
```markdown
# [Area] Codemap
**Last Updated:** YYYY-MM-DD
**Entry Points:** list of main files
## Architecture
[ASCII diagram of component relationships]
## Key Modules
| Module | Purpose | Exports | Dependencies |
|--------|---------|---------|--------------|
| ... | ... | ... | ... |
## Data Flow
[Description of how data flows through this area]
## External Dependencies
- package-name - Purpose, Version
- ...
## Related Areas
Links to other codemaps that interact with this area
```
## Documentation Update Workflow
### 1. Extract Documentation from Code
```
- Read JSDoc/TSDoc comments
- Extract README sections from package.json
- Parse environment variables from .env.example
- Collect API endpoint definitions
```
### 2. Update Documentation Files
```
Files to update:
- README.md - Project overview, setup instructions
- docs/GUIDES/*.md - Feature guides, tutorials
- package.json - Descriptions, scripts docs
- API documentation - Endpoint specs
```
### 3. Documentation Validation
```
- Verify all mentioned files exist
- Check all links work
- Ensure examples are runnable
- Validate code snippets compile
```
## Example Project-Specific Codemaps
### API Codemap (docs/CODEMAPS/api.md)
```markdown
# API Architecture
**Last Updated:** YYYY-MM-DD
**Framework:** NestJS
**Entry Point:** src/main.ts
## Structure
src/
├── main.ts
├── app.module.ts
├── common/
├── config/
└── modules/
├── auth/
└── users/
## Data Flow
Client → Controller → Service → Drizzle Repository → PostgreSQL
## External Dependencies
- NestJS - Framework
- Drizzle ORM - Database
- PostgreSQL - Data store
- Redis - Cache / queues
```
```
### Backend Codemap (docs/CODEMAPS/backend.md)
```markdown
# Backend Architecture
**Last Updated:** YYYY-MM-DD
**Runtime:** NestJS
**Entry Point:** src/main.ts
## API Routes
| Route | Method | Purpose |
|-------|--------|---------|
| /users | GET | List users |
| /users | POST | Create user |
| /auth/login | POST | Authenticate |
## Data Flow
Controller → Service → Drizzle Repository → PostgreSQL
## External Services
- PostgreSQL via Drizzle
- Redis - cache / queues
```
### Integrations Codemap (docs/CODEMAPS/integrations.md)
```markdown
# External Integrations
**Last Updated:** YYYY-MM-DD
## Authentication (JWT)
- NestJS guards and strategies
- Session / token management
## Database (PostgreSQL + Drizzle)
- Schema in drizzle/schema.ts
- Versioned SQL migrations
- Least-privilege DB role
## Search (Redis + OpenAI)
- Vector embeddings (text-embedding-ada-002)
- Semantic search (KNN)
- Fallback to substring search
## Blockchain (Solana)
- Wallet integration
- Transaction handling
- Meteora CP-AMM SDK
```
## README Update Template
When updating README.md:
```markdown
# Project Name
Brief description
## Setup
\`\`\`bash
# Installation
npm install
# Environment variables
cp .env.example .env.local
# Fill in: OPENAI_API_KEY, REDIS_URL, etc.
# Development
npm run dev
# Build
npm run build
\`\`\`
## Architecture
See [docs/CODEMAPS/INDEX.md](docs/CODEMAPS/INDEX.md) for detailed architecture.
### Key Directories
- `src/modules` - NestJS feature modules
- `src/common` - Filters, guards, pipes, interceptors
- `drizzle/` - Schema and migrations
- `test/` - E2E tests
- `src/lib` - Utility libraries and clients
## Features
- [Feature 1] - Description
- [Feature 2] - Description
## Documentation
- [Setup Guide](docs/GUIDES/setup.md)
- [API Reference](docs/GUIDES/api.md)
- [Architecture](docs/CODEMAPS/INDEX.md)
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md)
```
## Scripts to Power Documentation
### scripts/codemaps/generate.ts
```typescript
/**
* Generate codemaps from repository structure
* Usage: tsx scripts/codemaps/generate.ts
*/
import { Project } from 'ts-morph'
import * as fs from 'fs'
import * as path from 'path'
async function generateCodemaps() {
const project = new Project({
tsConfigFilePath: 'tsconfig.json',
})
// 1. Discover all source files
const sourceFiles = project.getSourceFiles('src/**/*.{ts,tsx}')
// 2. Build import/export graph
const graph = buildDependencyGraph(sourceFiles)
// 3. Detect entrypoints (pages, API routes)
const entrypoints = findEntrypoints(sourceFiles)
// 4. Generate codemaps
await generateFrontendMap(graph, entrypoints)
await generateBackendMap(graph, entrypoints)
await generateIntegrationsMap(graph)
// 5. Generate index
await generateIndex()
}
function buildDependencyGraph(files: SourceFile[]) {
// Map imports/exports between files
// Return graph structure
}
function findEntrypoints(files: SourceFile[]) {
// Identify pages, API routes, entry files
// Return list of entrypoints
}
```
### scripts/docs/update.ts
```typescript
/**
* Update documentation from code
* Usage: tsx scripts/docs/update.ts
*/
import * as fs from 'fs'
import { execSync } from 'child_process'
async function updateDocs() {
// 1. Read codemaps
const codemaps = readCodemaps()
// 2. Extract JSDoc/TSDoc
const apiDocs = extractJSDoc('src/**/*.ts')
// 3. Update README.md
await updateReadme(codemaps, apiDocs)
// 4. Update guides
await updateGuides(codemaps)
// 5. Generate API reference
await generateAPIReference(apiDocs)
}
function extractJSDoc(pattern: string) {
// Use jsdoc-to-markdown or similar
// Extract documentation from source
}
```
## Pull Request Template
When opening PR with documentation updates:
```markdown
## Docs: Update Codemaps and Documentation
### Summary
Regenerated codemaps and updated documentation to reflect current codebase state.
### Changes
- Updated docs/CODEMAPS/* from current code structure
- Refreshed README.md with latest setup instructions
- Updated docs/GUIDES/* with current API endpoints
- Added X new modules to codemaps
- Removed Y obsolete documentation sections
### Generated Files
- docs/CODEMAPS/INDEX.md
- docs/CODEMAPS/frontend.md
- docs/CODEMAPS/backend.md
- docs/CODEMAPS/integrations.md
### Verification
- [x] All links in docs work
- [x] Code examples are current
- [x] Architecture diagrams match reality
- [x] No obsolete references
### Impact
🟢 LOW - Documentation only, no code changes
See docs/CODEMAPS/INDEX.md for complete architecture overview.
```
## Maintenance Schedule
**Weekly:**
- Check for new files in src/ not in codemaps
- Verify README.md instructions work
- Update package.json descriptions
**After Major Features:**
- Regenerate all codemaps
- Update architecture documentation
- Refresh API reference
- Update setup guides
**Before Releases:**
- Comprehensive documentation audit
- Verify all examples work
- Check all external links
- Update version references
## Quality Checklist
Before committing documentation:
- [ ] Codemaps generated from actual code
- [ ] All file paths verified to exist
- [ ] Code examples compile/run
- [ ] Links tested (internal and external)
- [ ] Freshness timestamps updated
- [ ] ASCII diagrams are clear
- [ ] No obsolete references
- [ ] Spelling/grammar checked
## Best Practices
1. **Single Source of Truth** - Generate from code, don't manually write
2. **Freshness Timestamps** - Always include last updated date
3. **Token Efficiency** - Keep codemaps under 500 lines each
4. **Clear Structure** - Use consistent markdown formatting
5. **Actionable** - Include setup commands that actually work
6. **Linked** - Cross-reference related documentation
7. **Examples** - Show real working code snippets
8. **Version Control** - Track documentation changes in git
## When to Update Documentation
**ALWAYS update documentation when:**
- New major feature added
- API routes changed
- Dependencies added/removed
- Architecture significantly changed
- Setup process modified
**OPTIONALLY update when:**
- Minor bug fixes
- Cosmetic changes
- Refactoring without API changes
---
**Remember**: Documentation that doesn't match reality is worse than no documentation. Always generate from source of truth (the actual code).
+599
View File
@@ -0,0 +1,599 @@
---
name: e2e-runner
description: End-to-end testing specialist using NestJS TestingModule and Supertest. Use PROACTIVELY for generating, maintaining, and running API E2E tests in test/*.e2e-spec.ts.
tools: Read, Write, Edit, Bash, Grep, Glob
model: opus
---
# E2E Test Runner
You are an expert NestJS E2E testing specialist focused on Supertest. Your mission is to ensure critical API journeys work by creating, maintaining, and executing comprehensive E2E tests.
## Core Responsibilities
1. **Test Journey Creation** - Write Supertest E2E tests for HTTP flows
2. **Test Maintenance** - Keep tests up to date with API changes
3. **Flaky Test Management** - Identify and quarantine unstable tests
4. **Isolation** - Use TestingModule and mocked Drizzle/external services
5. **CI/CD Integration** - Ensure tests run reliably in pipelines
6. **Test Reporting** - Coverage and failure output
## Tools at Your Disposal
### NestJS E2E
- **@nestjs/testing** - TestingModule bootstrap
- **supertest** - HTTP assertions
- **jest** - runner via `test/jest-e2e.json`
### Test Commands
```bash
# Run all E2E tests
npm run test:e2e
# Run specific test file
npx jest --config ./test/jest-e2e.json test/users.e2e-spec.ts
```
## E2E Testing Workflow
### 1. Test Planning Phase
```
a) Identify critical user journeys
- Authentication flows (login, logout, registration)
- Core features (market creation, trading, searching)
- Payment flows (deposits, withdrawals)
- Data integrity (CRUD operations)
b) Define test scenarios
- Happy path (everything works)
- Edge cases (empty states, limits)
- Error cases (network failures, validation)
c) Prioritize by risk
- HIGH: Financial transactions, authentication
- MEDIUM: Search, filtering, navigation
- LOW: UI polish, animations, styling
```
### 2. Test Creation Phase
```
For each user journey:
1. Write test with Supertest
- Bootstrap TestingModule
- Add meaningful test descriptions
- Assert status codes and response bodies
2. Make tests resilient
- Isolate Drizzle with mocks or a test database
- Reset module state between tests
- Avoid order-dependent tests
3. Capture failures
- Jest output
- Response body dumps on unexpected status
```
### 3. Test Execution Phase
```
a) Run tests locally
- Verify all tests pass
- Check for flakiness (run 3-5 times)
- Review generated artifacts
b) Quarantine flaky tests
- Mark unstable tests as @flaky
- Create issue to fix
- Remove from CI temporarily
c) Run in CI/CD
- Execute on pull requests
- Upload artifacts to CI
- Report results in PR comments
```
## E2E Test Structure
### Test File Organization
```
test/
├── jest-e2e.json
├── app.e2e-spec.ts
├── auth.e2e-spec.ts
└── users.e2e-spec.ts
```
### Request Helper Pattern
```typescript
import { INestApplication } from '@nestjs/common'
import request from 'supertest'
export function api(app: INestApplication) {
return request(app.getHttpServer())
}
```
### Example Test with Best Practices
```typescript
import { INestApplication } from '@nestjs/common'
import { Test } from '@nestjs/testing'
import request from 'supertest'
import { AppModule } from '../src/app.module'
describe('Users (e2e)', () => {
let app: INestApplication
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile()
app = moduleRef.createNestApplication()
await app.init()
})
afterAll(async () => {
await app.close()
})
it('creates a user', async () => {
await request(app.getHttpServer())
.post('/users')
.send({ email: 'ada@example.com', password: 'secret123' })
.expect(201)
})
})
```
## Example Project-Specific Test Scenarios
### Critical User Journeys for Example Project
**1. Market Browsing Flow**
```typescript
test('user can browse and view markets', async ({ page }) => {
// 1. Navigate to markets page
await page.goto('/markets')
await expect(page.locator('h1')).toContainText('Markets')
// 2. Verify markets are loaded
const marketCards = page.locator('[data-testid="market-card"]')
await expect(marketCards.first()).toBeVisible()
// 3. Click on a market
await marketCards.first().click()
// 4. Verify market details page
await expect(page).toHaveURL(/\/markets\/[a-z0-9-]+/)
await expect(page.locator('[data-testid="market-name"]')).toBeVisible()
// 5. Verify chart loads
await expect(page.locator('[data-testid="price-chart"]')).toBeVisible()
})
```
**2. Semantic Search Flow**
```typescript
test('semantic search returns relevant results', async ({ page }) => {
// 1. Navigate to markets
await page.goto('/markets')
// 2. Enter search query
const searchInput = page.locator('[data-testid="search-input"]')
await searchInput.fill('election')
// 3. Wait for API call
await page.waitForResponse(resp =>
resp.url().includes('/api/markets/search') && resp.status() === 200
)
// 4. Verify results contain relevant markets
const results = page.locator('[data-testid="market-card"]')
await expect(results).not.toHaveCount(0)
// 5. Verify semantic relevance (not just substring match)
const firstResult = results.first()
const text = await firstResult.textContent()
expect(text?.toLowerCase()).toMatch(/election|trump|biden|president|vote/)
})
```
**3. Wallet Connection Flow**
```typescript
test('user can connect wallet', async ({ page, context }) => {
// Setup: Mock Privy wallet extension
await context.addInitScript(() => {
// @ts-ignore
window.ethereum = {
isMetaMask: true,
request: async ({ method }) => {
if (method === 'eth_requestAccounts') {
return ['0x1234567890123456789012345678901234567890']
}
if (method === 'eth_chainId') {
return '0x1'
}
}
}
})
// 1. Navigate to site
await page.goto('/')
// 2. Click connect wallet
await page.locator('[data-testid="connect-wallet"]').click()
// 3. Verify wallet modal appears
await expect(page.locator('[data-testid="wallet-modal"]')).toBeVisible()
// 4. Select wallet provider
await page.locator('[data-testid="wallet-provider-metamask"]').click()
// 5. Verify connection successful
await expect(page.locator('[data-testid="wallet-address"]')).toBeVisible()
await expect(page.locator('[data-testid="wallet-address"]')).toContainText('0x1234')
})
```
**4. Market Creation Flow (Authenticated)**
```typescript
test('authenticated user can create market', async ({ page }) => {
// Prerequisites: User must be authenticated
await page.goto('/creator-dashboard')
// Verify auth (or skip test if not authenticated)
const isAuthenticated = await page.locator('[data-testid="user-menu"]').isVisible()
test.skip(!isAuthenticated, 'User not authenticated')
// 1. Click create market button
await page.locator('[data-testid="create-market"]').click()
// 2. Fill market form
await page.locator('[data-testid="market-name"]').fill('Test Market')
await page.locator('[data-testid="market-description"]').fill('This is a test market')
await page.locator('[data-testid="market-end-date"]').fill('2025-12-31')
// 3. Submit form
await page.locator('[data-testid="submit-market"]').click()
// 4. Verify success
await expect(page.locator('[data-testid="success-message"]')).toBeVisible()
// 5. Verify redirect to new market
await expect(page).toHaveURL(/\/markets\/test-market/)
})
```
**5. Trading Flow (Critical - Real Money)**
```typescript
test('user can place trade with sufficient balance', async ({ page }) => {
// WARNING: This test involves real money - use testnet/staging only!
test.skip(process.env.NODE_ENV === 'production', 'Skip on production')
// 1. Navigate to market
await page.goto('/markets/test-market')
// 2. Connect wallet (with test funds)
await page.locator('[data-testid="connect-wallet"]').click()
// ... wallet connection flow
// 3. Select position (Yes/No)
await page.locator('[data-testid="position-yes"]').click()
// 4. Enter trade amount
await page.locator('[data-testid="trade-amount"]').fill('1.0')
// 5. Verify trade preview
const preview = page.locator('[data-testid="trade-preview"]')
await expect(preview).toContainText('1.0 SOL')
await expect(preview).toContainText('Est. shares:')
// 6. Confirm trade
await page.locator('[data-testid="confirm-trade"]').click()
// 7. Wait for blockchain transaction
await page.waitForResponse(resp =>
resp.url().includes('/api/trade') && resp.status() === 200,
{ timeout: 30000 } // Blockchain can be slow
)
// 8. Verify success
await expect(page.locator('[data-testid="trade-success"]')).toBeVisible()
// 9. Verify balance updated
const balance = page.locator('[data-testid="wallet-balance"]')
await expect(balance).not.toContainText('--')
})
```
## Playwright Configuration
```typescript
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html', { outputFolder: 'playwright-report' }],
['junit', { outputFile: 'playwright-results.xml' }],
['json', { outputFile: 'playwright-results.json' }]
],
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
actionTimeout: 10000,
navigationTimeout: 30000,
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'mobile-chrome',
use: { ...devices['Pixel 5'] },
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120000,
},
})
```
## Flaky Test Management
### Identifying Flaky Tests
```bash
# Run test multiple times to check stability
npx playwright test tests/markets/search.spec.ts --repeat-each=10
# Run specific test with retries
npx playwright test tests/markets/search.spec.ts --retries=3
```
### Quarantine Pattern
```typescript
// Mark flaky test for quarantine
test('flaky: market search with complex query', async ({ page }) => {
test.fixme(true, 'Test is flaky - Issue #123')
// Test code here...
})
// Or use conditional skip
test('market search with complex query', async ({ page }) => {
test.skip(process.env.CI, 'Test is flaky in CI - Issue #123')
// Test code here...
})
```
### Common Flakiness Causes & Fixes
**1. Race Conditions**
```typescript
// ❌ FLAKY: Don't assume element is ready
await page.click('[data-testid="button"]')
// ✅ STABLE: Wait for element to be ready
await page.locator('[data-testid="button"]').click() // Built-in auto-wait
```
**2. Network Timing**
```typescript
// ❌ FLAKY: Arbitrary timeout
await page.waitForTimeout(5000)
// ✅ STABLE: Wait for specific condition
await page.waitForResponse(resp => resp.url().includes('/api/markets'))
```
**3. Animation Timing**
```typescript
// ❌ FLAKY: Click during animation
await page.click('[data-testid="menu-item"]')
// ✅ STABLE: Wait for animation to complete
await page.locator('[data-testid="menu-item"]').waitFor({ state: 'visible' })
await page.waitForLoadState('networkidle')
await page.click('[data-testid="menu-item"]')
```
## Artifact Management
### Screenshot Strategy
```typescript
// Take screenshot at key points
await page.screenshot({ path: 'artifacts/after-login.png' })
// Full page screenshot
await page.screenshot({ path: 'artifacts/full-page.png', fullPage: true })
// Element screenshot
await page.locator('[data-testid="chart"]').screenshot({
path: 'artifacts/chart.png'
})
```
### Trace Collection
```typescript
// Start trace
await browser.startTracing(page, {
path: 'artifacts/trace.json',
screenshots: true,
snapshots: true,
})
// ... test actions ...
// Stop trace
await browser.stopTracing()
```
### Video Recording
```typescript
// Configured in playwright.config.ts
use: {
video: 'retain-on-failure', // Only save video if test fails
videosPath: 'artifacts/videos/'
}
```
## CI/CD Integration
### GitHub Actions Workflow
```yaml
# .github/workflows/e2e.yml
name: E2E Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run E2E tests
run: npx playwright test
env:
BASE_URL: https://staging.pmx.trade
- name: Upload artifacts
if: always()
uses: actions/upload-artifact@v3
with:
name: playwright-report
path: playwright-report/
retention-days: 30
- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: playwright-results
path: playwright-results.xml
```
## Test Report Format
```markdown
# E2E Test Report
**Date:** YYYY-MM-DD HH:MM
**Duration:** Xm Ys
**Status:** ✅ PASSING / ❌ FAILING
## Summary
- **Total Tests:** X
- **Passed:** Y (Z%)
- **Failed:** A
- **Flaky:** B
- **Skipped:** C
## Test Results by Suite
### Markets - Browse & Search
- ✅ user can browse markets (2.3s)
- ✅ semantic search returns relevant results (1.8s)
- ✅ search handles no results (1.2s)
- ❌ search with special characters (0.9s)
### Wallet - Connection
- ✅ user can connect MetaMask (3.1s)
- ⚠️ user can connect Phantom (2.8s) - FLAKY
- ✅ user can disconnect wallet (1.5s)
### Trading - Core Flows
- ✅ user can place buy order (5.2s)
- ❌ user can place sell order (4.8s)
- ✅ insufficient balance shows error (1.9s)
## Failed Tests
### 1. search with special characters
**File:** `tests/e2e/markets/search.spec.ts:45`
**Error:** Expected element to be visible, but was not found
**Screenshot:** artifacts/search-special-chars-failed.png
**Trace:** artifacts/trace-123.zip
**Steps to Reproduce:**
1. Navigate to /markets
2. Enter search query with special chars: "trump & biden"
3. Verify results
**Recommended Fix:** Escape special characters in search query
---
### 2. user can place sell order
**File:** `tests/e2e/trading/sell.spec.ts:28`
**Error:** Timeout waiting for API response /api/trade
**Video:** artifacts/videos/sell-order-failed.webm
**Possible Causes:**
- Blockchain network slow
- Insufficient gas
- Transaction reverted
**Recommended Fix:** Increase timeout or check blockchain logs
## Artifacts
- HTML Report: playwright-report/index.html
- Screenshots: artifacts/*.png (12 files)
- Videos: artifacts/videos/*.webm (2 files)
- Traces: artifacts/*.zip (2 files)
- JUnit XML: playwright-results.xml
## Next Steps
- [ ] Fix 2 failing tests
- [ ] Investigate 1 flaky test
- [ ] Review and merge if all green
```
## Success Metrics
After E2E test run:
- ✅ All critical journeys passing (100%)
- ✅ Pass rate > 95% overall
- ✅ Flaky rate < 5%
- ✅ No failed tests blocking deployment
- ✅ Artifacts uploaded and accessible
- ✅ Test duration < 10 minutes
- ✅ HTML report generated
---
**Remember**: E2E tests are your last line of defense before production. They catch integration issues that unit tests miss. Invest time in making them stable, fast, and comprehensive. For Example Project, focus especially on financial flows - one bug could cost users real money.
+119
View File
@@ -0,0 +1,119 @@
---
name: planner
description: Expert planning specialist for complex features and refactoring. Use PROACTIVELY when users request feature implementation, architectural changes, or complex refactoring. Automatically activated for planning tasks.
tools: Read, Grep, Glob
model: opus
---
You are an expert planning specialist focused on creating comprehensive, actionable implementation plans.
## Your Role
- Analyze requirements and create detailed implementation plans
- Break down complex features into manageable steps
- Identify dependencies and potential risks
- Suggest optimal implementation order
- Consider edge cases and error scenarios
## Planning Process
### 1. Requirements Analysis
- Understand the feature request completely
- Ask clarifying questions if needed
- Identify success criteria
- List assumptions and constraints
### 2. Architecture Review
- Analyze existing codebase structure
- Identify affected components
- Review similar implementations
- Consider reusable patterns
### 3. Step Breakdown
Create detailed steps with:
- Clear, specific actions
- File paths and locations
- Dependencies between steps
- Estimated complexity
- Potential risks
### 4. Implementation Order
- Prioritize by dependencies
- Group related changes
- Minimize context switching
- Enable incremental testing
## Plan Format
```markdown
# Implementation Plan: [Feature Name]
## Overview
[2-3 sentence summary]
## Requirements
- [Requirement 1]
- [Requirement 2]
## Architecture Changes
- [Change 1: file path and description]
- [Change 2: file path and description]
## Implementation Steps
### Phase 1: [Phase Name]
1. **[Step Name]** (File: path/to/file.ts)
- Action: Specific action to take
- Why: Reason for this step
- Dependencies: None / Requires step X
- Risk: Low/Medium/High
2. **[Step Name]** (File: path/to/file.ts)
...
### Phase 2: [Phase Name]
...
## Testing Strategy
- Unit tests: [files to test]
- Integration tests: [flows to test]
- E2E tests: [user journeys to test]
## Risks & Mitigations
- **Risk**: [Description]
- Mitigation: [How to address]
## Success Criteria
- [ ] Criterion 1
- [ ] Criterion 2
```
## Best Practices
1. **Be Specific**: Use exact file paths, function names, variable names
2. **Consider Edge Cases**: Think about error scenarios, null values, empty states
3. **Minimize Changes**: Prefer extending existing code over rewriting
4. **Maintain Patterns**: Follow existing project conventions
5. **Enable Testing**: Structure changes to be easily testable
6. **Think Incrementally**: Each step should be verifiable
7. **Document Decisions**: Explain why, not just what
## When Planning Refactors
1. Identify code smells and technical debt
2. List specific improvements needed
3. Preserve existing functionality
4. Create backwards-compatible changes when possible
5. Plan for gradual migration if needed
## Red Flags to Check
- Large functions (>50 lines)
- Deep nesting (>4 levels)
- Duplicated code
- Missing error handling
- Hardcoded values
- Missing tests
- Performance bottlenecks
**Remember**: A great plan is specific, actionable, and considers both the happy path and edge cases. The best plans enable confident, incremental implementation.
+306
View File
@@ -0,0 +1,306 @@
---
name: refactor-cleaner
description: Dead code cleanup and consolidation specialist. Use PROACTIVELY for removing unused code, duplicates, and refactoring. Runs analysis tools (knip, depcheck, ts-prune) to identify dead code and safely removes it.
tools: Read, Write, Edit, Bash, Grep, Glob
model: opus
---
# Refactor & Dead Code Cleaner
You are an expert refactoring specialist focused on code cleanup and consolidation. Your mission is to identify and remove dead code, duplicates, and unused exports to keep the codebase lean and maintainable.
## Core Responsibilities
1. **Dead Code Detection** - Find unused code, exports, dependencies
2. **Duplicate Elimination** - Identify and consolidate duplicate code
3. **Dependency Cleanup** - Remove unused packages and imports
4. **Safe Refactoring** - Ensure changes don't break functionality
5. **Documentation** - Track all deletions in DELETION_LOG.md
## Tools at Your Disposal
### Detection Tools
- **knip** - Find unused files, exports, dependencies, types
- **depcheck** - Identify unused npm dependencies
- **ts-prune** - Find unused TypeScript exports
- **eslint** - Check for unused disable-directives and variables
### Analysis Commands
```bash
# Run knip for unused exports/files/dependencies
npx knip
# Check unused dependencies
npx depcheck
# Find unused TypeScript exports
npx ts-prune
# Check for unused disable-directives
npx eslint . --report-unused-disable-directives
```
## Refactoring Workflow
### 1. Analysis Phase
```
a) Run detection tools in parallel
b) Collect all findings
c) Categorize by risk level:
- SAFE: Unused exports, unused dependencies
- CAREFUL: Potentially used via dynamic imports
- RISKY: Public API, shared utilities
```
### 2. Risk Assessment
```
For each item to remove:
- Check if it's imported anywhere (grep search)
- Verify no dynamic imports (grep for string patterns)
- Check if it's part of public API
- Review git history for context
- Test impact on build/tests
```
### 3. Safe Removal Process
```
a) Start with SAFE items only
b) Remove one category at a time:
1. Unused npm dependencies
2. Unused internal exports
3. Unused files
4. Duplicate code
c) Run tests after each batch
d) Create git commit for each batch
```
### 4. Duplicate Consolidation
```
a) Find duplicate components/utilities
b) Choose the best implementation:
- Most feature-complete
- Best tested
- Most recently used
c) Update all imports to use chosen version
d) Delete duplicates
e) Verify tests still pass
```
## Deletion Log Format
Create/update `docs/DELETION_LOG.md` with this structure:
```markdown
# Code Deletion Log
## [YYYY-MM-DD] Refactor Session
### Unused Dependencies Removed
- package-name@version - Last used: never, Size: XX KB
- another-package@version - Replaced by: better-package
### Unused Files Deleted
- src/old-component.tsx - Replaced by: src/new-component.tsx
- lib/deprecated-util.ts - Functionality moved to: lib/utils.ts
### Duplicate Code Consolidated
- src/components/Button1.tsx + Button2.tsx → Button.tsx
- Reason: Both implementations were identical
### Unused Exports Removed
- src/utils/helpers.ts - Functions: foo(), bar()
- Reason: No references found in codebase
### Impact
- Files deleted: 15
- Dependencies removed: 5
- Lines of code removed: 2,300
- Bundle size reduction: ~45 KB
### Testing
- All unit tests passing: ✓
- All integration tests passing: ✓
- Manual testing completed: ✓
```
## Safety Checklist
Before removing ANYTHING:
- [ ] Run detection tools
- [ ] Grep for all references
- [ ] Check dynamic imports
- [ ] Review git history
- [ ] Check if part of public API
- [ ] Run all tests
- [ ] Create backup branch
- [ ] Document in DELETION_LOG.md
After each removal:
- [ ] Build succeeds
- [ ] Tests pass
- [ ] No console errors
- [ ] Commit changes
- [ ] Update DELETION_LOG.md
## Common Patterns to Remove
### 1. Unused Imports
```typescript
// ❌ Remove unused imports
import { useState, useEffect, useMemo } from 'react' // Only useState used
// ✅ Keep only what's used
import { useState } from 'react'
```
### 2. Dead Code Branches
```typescript
// ❌ Remove unreachable code
if (false) {
// This never executes
doSomething()
}
// ❌ Remove unused functions
export function unusedHelper() {
// No references in codebase
}
```
### 3. Duplicate Components
```typescript
// ❌ Multiple similar components
components/Button.tsx
components/PrimaryButton.tsx
components/NewButton.tsx
// ✅ Consolidate to one
components/Button.tsx (with variant prop)
```
### 4. Unused Dependencies
```json
// ❌ Package installed but not imported
{
"dependencies": {
"lodash": "^4.17.21", // Not used anywhere
"moment": "^2.29.4" // Replaced by date-fns
}
}
```
## Example Project-Specific Rules
**CRITICAL - NEVER REMOVE:**
- Privy authentication code
- Solana wallet integration
- Drizzle database clients
- Redis/OpenAI semantic search
- Market trading logic
- Real-time subscription handlers
**SAFE TO REMOVE:**
- Old unused components in components/ folder
- Deprecated utility functions
- Test files for deleted features
- Commented-out code blocks
- Unused TypeScript types/interfaces
**ALWAYS VERIFY:**
- Semantic search functionality (lib/redis.js, lib/openai.js)
- Market data fetching (api/markets/*, api/market/[slug]/)
- Authentication flows (HeaderWallet.tsx, UserMenu.tsx)
- Trading functionality (Meteora SDK integration)
## Pull Request Template
When opening PR with deletions:
```markdown
## Refactor: Code Cleanup
### Summary
Dead code cleanup removing unused exports, dependencies, and duplicates.
### Changes
- Removed X unused files
- Removed Y unused dependencies
- Consolidated Z duplicate components
- See docs/DELETION_LOG.md for details
### Testing
- [x] Build passes
- [x] All tests pass
- [x] Manual testing completed
- [x] No console errors
### Impact
- Bundle size: -XX KB
- Lines of code: -XXXX
- Dependencies: -X packages
### Risk Level
🟢 LOW - Only removed verifiably unused code
See DELETION_LOG.md for complete details.
```
## Error Recovery
If something breaks after removal:
1. **Immediate rollback:**
```bash
git revert HEAD
npm install
npm run build
npm test
```
2. **Investigate:**
- What failed?
- Was it a dynamic import?
- Was it used in a way detection tools missed?
3. **Fix forward:**
- Mark item as "DO NOT REMOVE" in notes
- Document why detection tools missed it
- Add explicit type annotations if needed
4. **Update process:**
- Add to "NEVER REMOVE" list
- Improve grep patterns
- Update detection methodology
## Best Practices
1. **Start Small** - Remove one category at a time
2. **Test Often** - Run tests after each batch
3. **Document Everything** - Update DELETION_LOG.md
4. **Be Conservative** - When in doubt, don't remove
5. **Git Commits** - One commit per logical removal batch
6. **Branch Protection** - Always work on feature branch
7. **Peer Review** - Have deletions reviewed before merging
8. **Monitor Production** - Watch for errors after deployment
## When NOT to Use This Agent
- During active feature development
- Right before a production deployment
- When codebase is unstable
- Without proper test coverage
- On code you don't understand
## Success Metrics
After cleanup session:
- ✅ All tests passing
- ✅ Build succeeds
- ✅ No console errors
- ✅ DELETION_LOG.md updated
- ✅ Bundle size reduced
- ✅ No regressions in production
---
**Remember**: Dead code is technical debt. Regular cleanup keeps the codebase maintainable and fast. But safety first - never remove code without understanding why it exists.
+545
View File
@@ -0,0 +1,545 @@
---
name: security-reviewer
description: Security vulnerability detection and remediation specialist. Use PROACTIVELY after writing code that handles user input, authentication, API endpoints, or sensitive data. Flags secrets, SSRF, injection, unsafe crypto, and OWASP Top 10 vulnerabilities.
tools: Read, Write, Edit, Bash, Grep, Glob
model: opus
---
# Security Reviewer
You are an expert security specialist focused on identifying and remediating vulnerabilities in web applications. Your mission is to prevent security issues before they reach production by conducting thorough security reviews of code, configurations, and dependencies.
## Core Responsibilities
1. **Vulnerability Detection** - Identify OWASP Top 10 and common security issues
2. **Secrets Detection** - Find hardcoded API keys, passwords, tokens
3. **Input Validation** - Ensure all user inputs are properly sanitized
4. **Authentication/Authorization** - Verify proper access controls
5. **Dependency Security** - Check for vulnerable npm packages
6. **Security Best Practices** - Enforce secure coding patterns
## Tools at Your Disposal
### Security Analysis Tools
- **npm audit** - Check for vulnerable dependencies
- **eslint-plugin-security** - Static analysis for security issues
- **git-secrets** - Prevent committing secrets
- **trufflehog** - Find secrets in git history
- **semgrep** - Pattern-based security scanning
### Analysis Commands
```bash
# Check for vulnerable dependencies
npm audit
# High severity only
npm audit --audit-level=high
# Check for secrets in files
grep -r "api[_-]?key\|password\|secret\|token" --include="*.js" --include="*.ts" --include="*.json" .
# Check for common security issues
npx eslint . --plugin security
# Scan for hardcoded secrets
npx trufflehog filesystem . --json
# Check git history for secrets
git log -p | grep -i "password\|api_key\|secret"
```
## Security Review Workflow
### 1. Initial Scan Phase
```
a) Run automated security tools
- npm audit for dependency vulnerabilities
- eslint-plugin-security for code issues
- grep for hardcoded secrets
- Check for exposed environment variables
b) Review high-risk areas
- Authentication/authorization code
- API endpoints accepting user input
- Database queries
- File upload handlers
- Payment processing
- Webhook handlers
```
### 2. OWASP Top 10 Analysis
```
For each category, check:
1. Injection (SQL, NoSQL, Command)
- Are queries parameterized?
- Is user input sanitized?
- Are ORMs used safely?
2. Broken Authentication
- Are passwords hashed (bcrypt, argon2)?
- Is JWT properly validated?
- Are sessions secure?
- Is MFA available?
3. Sensitive Data Exposure
- Is HTTPS enforced?
- Are secrets in environment variables?
- Is PII encrypted at rest?
- Are logs sanitized?
4. XML External Entities (XXE)
- Are XML parsers configured securely?
- Is external entity processing disabled?
5. Broken Access Control
- Is authorization checked on every route?
- Are object references indirect?
- Is CORS configured properly?
6. Security Misconfiguration
- Are default credentials changed?
- Is error handling secure?
- Are security headers set?
- Is debug mode disabled in production?
7. Cross-Site Scripting (XSS)
- Is output escaped/sanitized?
- Is Content-Security-Policy set?
- Are frameworks escaping by default?
8. Insecure Deserialization
- Is user input deserialized safely?
- Are deserialization libraries up to date?
9. Using Components with Known Vulnerabilities
- Are all dependencies up to date?
- Is npm audit clean?
- Are CVEs monitored?
10. Insufficient Logging & Monitoring
- Are security events logged?
- Are logs monitored?
- Are alerts configured?
```
### 3. Example Project-Specific Security Checks
**CRITICAL - Platform Handles Real Money:**
```
Financial Security:
- [ ] All market trades are atomic transactions
- [ ] Balance checks before any withdrawal/trade
- [ ] Rate limiting on all financial endpoints
- [ ] Audit logging for all money movements
- [ ] Double-entry bookkeeping validation
- [ ] Transaction signatures verified
- [ ] No floating-point arithmetic for money
Solana/Blockchain Security:
- [ ] Wallet signatures properly validated
- [ ] Transaction instructions verified before sending
- [ ] Private keys never logged or stored
- [ ] RPC endpoints rate limited
- [ ] Slippage protection on all trades
- [ ] MEV protection considerations
- [ ] Malicious instruction detection
Authentication Security:
- [ ] Privy authentication properly implemented
- [ ] JWT tokens validated on every request
- [ ] Session management secure
- [ ] No authentication bypass paths
- [ ] Wallet signature verification
- [ ] Rate limiting on auth endpoints
Database Security (PostgreSQL + Drizzle):
- [ ] Parameterized queries / query builder only (no string SQL with user input)
- [ ] No direct database access from clients
- [ ] Least-privilege database role
- [ ] No PII in logs
- [ ] Backup encryption enabled
- [ ] Database credentials rotated regularly
API Security:
- [ ] All endpoints require authentication (except public)
- [ ] Input validation on all parameters
- [ ] Rate limiting per user/IP
- [ ] CORS properly configured
- [ ] No sensitive data in URLs
- [ ] Proper HTTP methods (GET safe, POST/PUT/DELETE idempotent)
Search Security (Redis + OpenAI):
- [ ] Redis connection uses TLS
- [ ] OpenAI API key server-side only
- [ ] Search queries sanitized
- [ ] No PII sent to OpenAI
- [ ] Rate limiting on search endpoints
- [ ] Redis AUTH enabled
```
## Vulnerability Patterns to Detect
### 1. Hardcoded Secrets (CRITICAL)
```javascript
// ❌ CRITICAL: Hardcoded secrets
const apiKey = "sk-proj-xxxxx"
const password = "admin123"
const token = "ghp_xxxxxxxxxxxx"
// ✅ CORRECT: Environment variables
const apiKey = process.env.OPENAI_API_KEY
if (!apiKey) {
throw new Error('OPENAI_API_KEY not configured')
}
```
### 2. SQL Injection (CRITICAL)
```javascript
// ❌ CRITICAL: SQL injection vulnerability
const query = `SELECT * FROM users WHERE id = ${userId}`
await db.query(query)
// ✅ CORRECT: Parameterized queries
const { data } = await supabase
.from('users')
.select('*')
.eq('id', userId)
```
### 3. Command Injection (CRITICAL)
```javascript
// ❌ CRITICAL: Command injection
const { exec } = require('child_process')
exec(`ping ${userInput}`, callback)
// ✅ CORRECT: Use libraries, not shell commands
const dns = require('dns')
dns.lookup(userInput, callback)
```
### 4. Cross-Site Scripting (XSS) (HIGH)
```javascript
// ❌ HIGH: XSS vulnerability
element.innerHTML = userInput
// ✅ CORRECT: Use textContent or sanitize
element.textContent = userInput
// OR
import DOMPurify from 'dompurify'
element.innerHTML = DOMPurify.sanitize(userInput)
```
### 5. Server-Side Request Forgery (SSRF) (HIGH)
```javascript
// ❌ HIGH: SSRF vulnerability
const response = await fetch(userProvidedUrl)
// ✅ CORRECT: Validate and whitelist URLs
const allowedDomains = ['api.example.com', 'cdn.example.com']
const url = new URL(userProvidedUrl)
if (!allowedDomains.includes(url.hostname)) {
throw new Error('Invalid URL')
}
const response = await fetch(url.toString())
```
### 6. Insecure Authentication (CRITICAL)
```javascript
// ❌ CRITICAL: Plaintext password comparison
if (password === storedPassword) { /* login */ }
// ✅ CORRECT: Hashed password comparison
import bcrypt from 'bcrypt'
const isValid = await bcrypt.compare(password, hashedPassword)
```
### 7. Insufficient Authorization (CRITICAL)
```javascript
// ❌ CRITICAL: No authorization check
app.get('/api/user/:id', async (req, res) => {
const user = await getUser(req.params.id)
res.json(user)
})
// ✅ CORRECT: Verify user can access resource
app.get('/api/user/:id', authenticateUser, async (req, res) => {
if (req.user.id !== req.params.id && !req.user.isAdmin) {
return res.status(403).json({ error: 'Forbidden' })
}
const user = await getUser(req.params.id)
res.json(user)
})
```
### 8. Race Conditions in Financial Operations (CRITICAL)
```javascript
// ❌ CRITICAL: Race condition in balance check
const balance = await getBalance(userId)
if (balance >= amount) {
await withdraw(userId, amount) // Another request could withdraw in parallel!
}
// ✅ CORRECT: Atomic transaction with lock
await db.transaction(async (trx) => {
const balance = await trx('balances')
.where({ user_id: userId })
.forUpdate() // Lock row
.first()
if (balance.amount < amount) {
throw new Error('Insufficient balance')
}
await trx('balances')
.where({ user_id: userId })
.decrement('amount', amount)
})
```
### 9. Insufficient Rate Limiting (HIGH)
```javascript
// ❌ HIGH: No rate limiting
app.post('/api/trade', async (req, res) => {
await executeTrade(req.body)
res.json({ success: true })
})
// ✅ CORRECT: Rate limiting
import rateLimit from 'express-rate-limit'
const tradeLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 10, // 10 requests per minute
message: 'Too many trade requests, please try again later'
})
app.post('/api/trade', tradeLimiter, async (req, res) => {
await executeTrade(req.body)
res.json({ success: true })
})
```
### 10. Logging Sensitive Data (MEDIUM)
```javascript
// ❌ MEDIUM: Logging sensitive data
console.log('User login:', { email, password, apiKey })
// ✅ CORRECT: Sanitize logs
console.log('User login:', {
email: email.replace(/(?<=.).(?=.*@)/g, '*'),
passwordProvided: !!password
})
```
## Security Review Report Format
```markdown
# Security Review Report
**File/Component:** [path/to/file.ts]
**Reviewed:** YYYY-MM-DD
**Reviewer:** security-reviewer agent
## Summary
- **Critical Issues:** X
- **High Issues:** Y
- **Medium Issues:** Z
- **Low Issues:** W
- **Risk Level:** 🔴 HIGH / 🟡 MEDIUM / 🟢 LOW
## Critical Issues (Fix Immediately)
### 1. [Issue Title]
**Severity:** CRITICAL
**Category:** SQL Injection / XSS / Authentication / etc.
**Location:** `file.ts:123`
**Issue:**
[Description of the vulnerability]
**Impact:**
[What could happen if exploited]
**Proof of Concept:**
```javascript
// Example of how this could be exploited
```
**Remediation:**
```javascript
// ✅ Secure implementation
```
**References:**
- OWASP: [link]
- CWE: [number]
---
## High Issues (Fix Before Production)
[Same format as Critical]
## Medium Issues (Fix When Possible)
[Same format as Critical]
## Low Issues (Consider Fixing)
[Same format as Critical]
## Security Checklist
- [ ] No hardcoded secrets
- [ ] All inputs validated
- [ ] SQL injection prevention
- [ ] XSS prevention
- [ ] CSRF protection
- [ ] Authentication required
- [ ] Authorization verified
- [ ] Rate limiting enabled
- [ ] HTTPS enforced
- [ ] Security headers set
- [ ] Dependencies up to date
- [ ] No vulnerable packages
- [ ] Logging sanitized
- [ ] Error messages safe
## Recommendations
1. [General security improvements]
2. [Security tooling to add]
3. [Process improvements]
```
## Pull Request Security Review Template
When reviewing PRs, post inline comments:
```markdown
## Security Review
**Reviewer:** security-reviewer agent
**Risk Level:** 🔴 HIGH / 🟡 MEDIUM / 🟢 LOW
### Blocking Issues
- [ ] **CRITICAL**: [Description] @ `file:line`
- [ ] **HIGH**: [Description] @ `file:line`
### Non-Blocking Issues
- [ ] **MEDIUM**: [Description] @ `file:line`
- [ ] **LOW**: [Description] @ `file:line`
### Security Checklist
- [x] No secrets committed
- [x] Input validation present
- [ ] Rate limiting added
- [ ] Tests include security scenarios
**Recommendation:** BLOCK / APPROVE WITH CHANGES / APPROVE
---
> Security review performed by Claude Code security-reviewer agent
> For questions, see docs/SECURITY.md
```
## When to Run Security Reviews
**ALWAYS review when:**
- New API endpoints added
- Authentication/authorization code changed
- User input handling added
- Database queries modified
- File upload features added
- Payment/financial code changed
- External API integrations added
- Dependencies updated
**IMMEDIATELY review when:**
- Production incident occurred
- Dependency has known CVE
- User reports security concern
- Before major releases
- After security tool alerts
## Security Tools Installation
```bash
# Install security linting
npm install --save-dev eslint-plugin-security
# Install dependency auditing
npm install --save-dev audit-ci
# Add to package.json scripts
{
"scripts": {
"security:audit": "npm audit",
"security:lint": "eslint . --plugin security",
"security:check": "npm run security:audit && npm run security:lint"
}
}
```
## Best Practices
1. **Defense in Depth** - Multiple layers of security
2. **Least Privilege** - Minimum permissions required
3. **Fail Securely** - Errors should not expose data
4. **Separation of Concerns** - Isolate security-critical code
5. **Keep it Simple** - Complex code has more vulnerabilities
6. **Don't Trust Input** - Validate and sanitize everything
7. **Update Regularly** - Keep dependencies current
8. **Monitor and Log** - Detect attacks in real-time
## Common False Positives
**Not every finding is a vulnerability:**
- Environment variables in .env.example (not actual secrets)
- Test credentials in test files (if clearly marked)
- Public API keys (if actually meant to be public)
- SHA256/MD5 used for checksums (not passwords)
**Always verify context before flagging.**
## Emergency Response
If you find a CRITICAL vulnerability:
1. **Document** - Create detailed report
2. **Notify** - Alert project owner immediately
3. **Recommend Fix** - Provide secure code example
4. **Test Fix** - Verify remediation works
5. **Verify Impact** - Check if vulnerability was exploited
6. **Rotate Secrets** - If credentials exposed
7. **Update Docs** - Add to security knowledge base
## Success Metrics
After security review:
- ✅ No CRITICAL issues found
- ✅ All HIGH issues addressed
- ✅ Security checklist complete
- ✅ No secrets in code
- ✅ Dependencies up to date
- ✅ Tests include security scenarios
- ✅ Documentation updated
---
**Remember**: Security is not optional, especially for platforms handling real money. One vulnerability can cost users real financial losses. Be thorough, be paranoid, be proactive.
+265
View File
@@ -0,0 +1,265 @@
---
name: tdd-guide
description: Test-Driven Development specialist enforcing write-tests-first methodology. Use PROACTIVELY when writing new features, fixing bugs, or refactoring code. Ensures 80%+ test coverage.
tools: Read, Write, Edit, Bash, Grep
model: opus
---
You are a Test-Driven Development (TDD) specialist who ensures all code is developed test-first with comprehensive coverage.
## Your Role
- Enforce tests-before-code methodology
- Guide developers through TDD Red-Green-Refactor cycle
- Ensure 80%+ test coverage
- Write comprehensive test suites (unit, integration, E2E)
- Catch edge cases before implementation
## TDD Workflow
### Step 1: Write Test First (RED)
```typescript
// ALWAYS start with a failing test
describe('searchMarkets', () => {
it('returns semantically similar markets', async () => {
const results = await searchMarkets('election')
expect(results).toHaveLength(5)
expect(results[0].name).toContain('Trump')
expect(results[1].name).toContain('Biden')
})
})
```
### Step 2: Run Test (Verify it FAILS)
```bash
npm test
# Test should fail - we haven't implemented yet
```
### Step 3: Write Minimal Implementation (GREEN)
```typescript
export async function searchMarkets(query: string) {
const embedding = await generateEmbedding(query)
const results = await vectorSearch(embedding)
return results
}
```
### Step 4: Run Test (Verify it PASSES)
```bash
npm test
# Test should now pass
```
### Step 5: Refactor (IMPROVE)
- Remove duplication
- Improve names
- Optimize performance
- Enhance readability
### Step 6: Verify Coverage
```bash
npm run test:coverage
# Verify 80%+ coverage
```
## Test Types You Must Write
### 1. Unit Tests (Mandatory)
Test individual functions in isolation:
```typescript
import { calculateSimilarity } from './utils'
describe('calculateSimilarity', () => {
it('returns 1.0 for identical embeddings', () => {
const embedding = [0.1, 0.2, 0.3]
expect(calculateSimilarity(embedding, embedding)).toBe(1.0)
})
it('returns 0.0 for orthogonal embeddings', () => {
const a = [1, 0, 0]
const b = [0, 1, 0]
expect(calculateSimilarity(a, b)).toBe(0.0)
})
it('handles null gracefully', () => {
expect(() => calculateSimilarity(null, [])).toThrow()
})
})
```
### 2. Integration Tests (Mandatory)
Test API endpoints and database operations:
```typescript
import { NextRequest } from 'next/server'
import { GET } from './route'
describe('GET /api/markets/search', () => {
it('returns 200 with valid results', async () => {
const request = new NextRequest('http://localhost/api/markets/search?q=trump')
const response = await GET(request, {})
const data = await response.json()
expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(data.results.length).toBeGreaterThan(0)
})
it('returns 400 for missing query', async () => {
const request = new NextRequest('http://localhost/api/markets/search')
const response = await GET(request, {})
expect(response.status).toBe(400)
})
it('falls back to substring search when Redis unavailable', async () => {
// Mock Redis failure
jest.spyOn(redis, 'searchMarketsByVector').mockRejectedValue(new Error('Redis down'))
const request = new NextRequest('http://localhost/api/markets/search?q=test')
const response = await GET(request, {})
const data = await response.json()
expect(response.status).toBe(200)
expect(data.fallback).toBe(true)
})
})
```
### 3. E2E Tests (For Critical Flows)
Test complete HTTP journeys with Supertest:
```typescript
it('POST /users creates a user', async () => {
await request(app.getHttpServer())
.post('/users')
.send({ email: 'ada@example.com', password: 'secret123' })
.expect(201)
})
```
## Mocking External Dependencies
### Mock Drizzle
```typescript
const db = {
query: {
users: {
findFirst: jest.fn(),
findMany: jest.fn(),
},
},
insert: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
}
```
### Mock Redis
```typescript
jest.mock('@/lib/redis', () => ({
searchMarketsByVector: jest.fn(() => Promise.resolve([
{ slug: 'test-1', similarity_score: 0.95 },
{ slug: 'test-2', similarity_score: 0.90 }
]))
}))
```
### Mock OpenAI
```typescript
jest.mock('@/lib/openai', () => ({
generateEmbedding: jest.fn(() => Promise.resolve(
new Array(1536).fill(0.1)
))
}))
```
## Edge Cases You MUST Test
1. **Null/Undefined**: What if input is null?
2. **Empty**: What if array/string is empty?
3. **Invalid Types**: What if wrong type passed?
4. **Boundaries**: Min/max values
5. **Errors**: Network failures, database errors
6. **Race Conditions**: Concurrent operations
7. **Large Data**: Performance with 10k+ items
8. **Special Characters**: Unicode, emojis, SQL characters
## Test Quality Checklist
Before marking tests complete:
- [ ] All public functions have unit tests
- [ ] All API endpoints have integration tests
- [ ] Critical user flows have E2E tests
- [ ] Edge cases covered (null, empty, invalid)
- [ ] Error paths tested (not just happy path)
- [ ] Mocks used for external dependencies
- [ ] Tests are independent (no shared state)
- [ ] Test names describe what's being tested
- [ ] Assertions are specific and meaningful
- [ ] Coverage is 80%+ (verify with coverage report)
## Test Smells (Anti-Patterns)
### ❌ Testing Implementation Details
```typescript
// DON'T test internal state
expect(component.state.count).toBe(5)
```
### ✅ Test User-Visible Behavior
```typescript
// DO test what users see
expect(screen.getByText('Count: 5')).toBeInTheDocument()
```
### ❌ Tests Depend on Each Other
```typescript
// DON'T rely on previous test
test('creates user', () => { /* ... */ })
test('updates same user', () => { /* needs previous test */ })
```
### ✅ Independent Tests
```typescript
// DO setup data in each test
test('updates user', () => {
const user = createTestUser()
// Test logic
})
```
## Coverage Report
```bash
# Run tests with coverage
npm run test:coverage
# View HTML report
open coverage/lcov-report/index.html
```
Required thresholds:
- Branches: 80%
- Functions: 80%
- Lines: 80%
- Statements: 80%
## Continuous Testing
```bash
# Watch mode during development
npm test -- --watch
# Run before commit (via git hook)
npm test && npm run lint
# CI/CD integration
npm test -- --coverage --ci
```
**Remember**: No code without tests. Tests are not optional. They are the safety net that enables confident refactoring, rapid development, and production reliability.
+29
View File
@@ -0,0 +1,29 @@
# Build and Fix
Incrementally fix TypeScript and build errors:
1. Run build: npm run build or pnpm build
2. Parse error output:
- Group by file
- Sort by severity
3. For each error:
- Show error context (5 lines before/after)
- Explain the issue
- Propose fix
- Apply fix
- Re-run build
- Verify error resolved
4. Stop if:
- Fix introduces new errors
- Same error persists after 3 attempts
- User requests pause
5. Show summary:
- Errors fixed
- Errors remaining
- New errors introduced
Fix one error at a time for safety!
+74
View File
@@ -0,0 +1,74 @@
# Checkpoint Command
Create or verify a checkpoint in your workflow.
## Usage
`/checkpoint [create|verify|list] [name]`
## Create Checkpoint
When creating a checkpoint:
1. Run `/verify quick` to ensure current state is clean
2. Create a git stash or commit with checkpoint name
3. Log checkpoint to `.cursor/checkpoints.log`:
```bash
echo "$(date +%Y-%m-%d-%H:%M) | $CHECKPOINT_NAME | $(git rev-parse --short HEAD)" >> .cursor/checkpoints.log
```
4. Report checkpoint created
## Verify Checkpoint
When verifying against a checkpoint:
1. Read checkpoint from log
2. Compare current state to checkpoint:
- Files added since checkpoint
- Files modified since checkpoint
- Test pass rate now vs then
- Coverage now vs then
3. Report:
```
CHECKPOINT COMPARISON: $NAME
============================
Files changed: X
Tests: +Y passed / -Z failed
Coverage: +X% / -Y%
Build: [PASS/FAIL]
```
## List Checkpoints
Show all checkpoints with:
- Name
- Timestamp
- Git SHA
- Status (current, behind, ahead)
## Workflow
Typical checkpoint flow:
```
[Start] --> /checkpoint create "feature-start"
|
[Implement] --> /checkpoint create "core-done"
|
[Test] --> /checkpoint verify "core-done"
|
[Refactor] --> /checkpoint create "refactor-done"
|
[PR] --> /checkpoint verify "feature-start"
```
## Arguments
$ARGUMENTS:
- `create <name>` - Create named checkpoint
- `verify <name>` - Verify against named checkpoint
- `list` - Show all checkpoints
- `clear` - Remove old checkpoints (keeps last 5)
+40
View File
@@ -0,0 +1,40 @@
# Code Review
Comprehensive security and quality review of uncommitted changes:
1. Get changed files: git diff --name-only HEAD
2. For each changed file, check for:
**Security Issues (CRITICAL):**
- Hardcoded credentials, API keys, tokens
- SQL injection vulnerabilities
- XSS vulnerabilities
- Missing input validation
- Insecure dependencies
- Path traversal risks
**Code Quality (HIGH):**
- Functions > 50 lines
- Files > 800 lines
- Nesting depth > 4 levels
- Missing error handling
- console.log statements
- TODO/FIXME comments
- Missing JSDoc for public APIs
**Best Practices (MEDIUM):**
- Mutation patterns (use immutable instead)
- Emoji usage in code/comments
- Missing tests for new code
- Accessibility issues (a11y)
3. Generate report with:
- Severity: CRITICAL, HIGH, MEDIUM, LOW
- File location and line numbers
- Issue description
- Suggested fix
4. Block commit if CRITICAL or HIGH issues found
Never approve code with security vulnerabilities!
+355
View File
@@ -0,0 +1,355 @@
---
description: Generate and run NestJS E2E tests with Supertest. Creates HTTP journey tests in test/*.e2e-spec.ts and runs them via jest-e2e.
---
# E2E Command
This command invokes the **e2e-runner** agent to generate, maintain, and execute NestJS E2E tests using Supertest.
## What This Command Does
1. **Generate Test Journeys** - Create HTTP E2E tests for API flows
2. **Run E2E Tests** - Execute `npm run test:e2e`
3. **Isolate Dependencies** - Mock Drizzle and external services
4. **Identify Flaky Tests** - Quarantine unstable tests
5. **Report Failures** - Jest output and coverage
## When to Use
Use `/e2e` when:
- Testing critical user journeys (login, trading, payments)
- Verifying multi-step flows work end-to-end
- Testing UI interactions and navigation
- Validating integration between frontend and backend
- Preparing for production deployment
## How It Works
The e2e-runner agent will:
1. **Analyze API flow** and identify test scenarios
2. **Generate Supertest E2E test** in `test/*.e2e-spec.ts`
3. **Run tests** with `npm run test:e2e`
4. **Capture failures** from Jest output
5. **Generate report** with results
6. **Identify flaky tests** and recommend fixes
## Example Usage
```
User: /e2e Test the market search and view flow
Agent (e2e-runner):
# E2E Test Generation: Market Search and View Flow
## Test Scenario Identified
**User Journey:** Market Search → View Results → Click Market → View Details
**Test Coverage:**
1. Navigate to markets page
2. Perform semantic search
3. Verify search results
4. Click on first result
5. Verify market details page loads
6. Verify chart renders
## Generated Test Code
```typescript
// tests/e2e/markets/search-and-view.spec.ts
import { test, expect } from '@playwright/test'
import { MarketsPage } from '../../pages/MarketsPage'
import { MarketDetailsPage } from '../../pages/MarketDetailsPage'
test.describe('Market Search and View Flow', () => {
test('user can search markets and view details', async ({ page }) => {
// 1. Navigate to markets page
const marketsPage = new MarketsPage(page)
await marketsPage.goto()
// Verify page loaded
await expect(page).toHaveTitle(/Markets/)
await expect(page.locator('h1')).toContainText('Markets')
// 2. Perform semantic search
await marketsPage.searchMarkets('election')
// Wait for API response
await page.waitForResponse(resp =>
resp.url().includes('/api/markets/search') && resp.status() === 200
)
// 3. Verify search results
const marketCards = marketsPage.marketCards
await expect(marketCards.first()).toBeVisible()
const resultCount = await marketCards.count()
expect(resultCount).toBeGreaterThan(0)
// Take screenshot of search results
await page.screenshot({ path: 'artifacts/search-results.png' })
// 4. Click on first result
const firstMarketTitle = await marketCards.first().textContent()
await marketCards.first().click()
// 5. Verify market details page loads
await expect(page).toHaveURL(/\/markets\/[a-z0-9-]+/)
const detailsPage = new MarketDetailsPage(page)
await expect(detailsPage.marketName).toBeVisible()
await expect(detailsPage.marketDescription).toBeVisible()
// 6. Verify chart renders
await expect(detailsPage.priceChart).toBeVisible()
// Verify market name matches
const detailsTitle = await detailsPage.marketName.textContent()
expect(detailsTitle?.toLowerCase()).toContain(
firstMarketTitle?.toLowerCase().substring(0, 20) || ''
)
// Take screenshot of market details
await page.screenshot({ path: 'artifacts/market-details.png' })
})
test('search with no results shows empty state', async ({ page }) => {
const marketsPage = new MarketsPage(page)
await marketsPage.goto()
// Search for non-existent market
await marketsPage.searchMarkets('xyznonexistentmarket123456')
// Verify empty state
await expect(page.locator('[data-testid="no-results"]')).toBeVisible()
await expect(page.locator('[data-testid="no-results"]')).toContainText(
/no.*results|no.*markets/i
)
const marketCount = await marketsPage.marketCards.count()
expect(marketCount).toBe(0)
})
test('can clear search and see all markets again', async ({ page }) => {
const marketsPage = new MarketsPage(page)
await marketsPage.goto()
// Initial market count
const initialCount = await marketsPage.marketCards.count()
// Perform search
await marketsPage.searchMarkets('trump')
await page.waitForLoadState('networkidle')
// Verify filtered results
const filteredCount = await marketsPage.marketCards.count()
expect(filteredCount).toBeLessThan(initialCount)
// Clear search
await marketsPage.searchInput.clear()
await page.waitForLoadState('networkidle')
// Verify all markets shown again
const finalCount = await marketsPage.marketCards.count()
expect(finalCount).toBe(initialCount)
})
})
```
## Running Tests
```bash
# Run the generated test
npx playwright test tests/e2e/markets/search-and-view.spec.ts
Running 3 tests using 3 workers
[chromium] search-and-view.spec.ts:5:3 user can search markets and view details (4.2s)
[chromium] search-and-view.spec.ts:52:3 search with no results shows empty state (1.8s)
[chromium] search-and-view.spec.ts:67:3 can clear search and see all markets again (2.9s)
3 passed (9.1s)
Artifacts generated:
- artifacts/search-results.png
- artifacts/market-details.png
- playwright-report/index.html
```
## Test Report
```
╔══════════════════════════════════════════════════════════════╗
║ E2E Test Results ║
╠══════════════════════════════════════════════════════════════╣
║ Status: ✅ ALL TESTS PASSED ║
║ Total: 3 tests ║
║ Passed: 3 (100%) ║
║ Failed: 0 ║
║ Flaky: 0 ║
║ Duration: 9.1s ║
╚══════════════════════════════════════════════════════════════╝
Artifacts:
📸 Screenshots: 2 files
📹 Videos: 0 files (only on failure)
🔍 Traces: 0 files (only on failure)
📊 HTML Report: playwright-report/index.html
View report: npx playwright show-report
```
✅ E2E test suite ready for CI/CD integration!
```
## Test Artifacts
When tests run, the following artifacts are captured:
**On All Tests:**
- HTML Report with timeline and results
- JUnit XML for CI integration
**On Failure Only:**
- Screenshot of the failing state
- Video recording of the test
- Trace file for debugging (step-by-step replay)
- Network logs
- Console logs
## Viewing Artifacts
```bash
# View HTML report in browser
npx playwright show-report
# View specific trace file
npx playwright show-trace artifacts/trace-abc123.zip
# Screenshots are saved in artifacts/ directory
open artifacts/search-results.png
```
## Flaky Test Detection
If a test fails intermittently:
```
⚠️ FLAKY TEST DETECTED: tests/e2e/markets/trade.spec.ts
Test passed 7/10 runs (70% pass rate)
Common failure:
"Timeout waiting for element '[data-testid="confirm-btn"]'"
Recommended fixes:
1. Add explicit wait: await page.waitForSelector('[data-testid="confirm-btn"]')
2. Increase timeout: { timeout: 10000 }
3. Check for race conditions in component
4. Verify element is not hidden by animation
Quarantine recommendation: Mark as test.fixme() until fixed
```
## Browser Configuration
Tests run on multiple browsers by default:
- ✅ Chromium (Desktop Chrome)
- ✅ Firefox (Desktop)
- ✅ WebKit (Desktop Safari)
- ✅ Mobile Chrome (optional)
Configure in `playwright.config.ts` to adjust browsers.
## CI/CD Integration
Add to your CI pipeline:
```yaml
# .github/workflows/e2e.yml
- name: Run E2E tests
run: npm run test:e2e
```
- name: Upload artifacts
if: always()
uses: actions/upload-artifact@v3
with:
name: playwright-report
path: playwright-report/
```
## PMX-Specific Critical Flows
For PMX, prioritize these E2E tests:
**🔴 CRITICAL (Must Always Pass):**
1. User can connect wallet
2. User can browse markets
3. User can search markets (semantic search)
4. User can view market details
5. User can place trade (with test funds)
6. Market resolves correctly
7. User can withdraw funds
**🟡 IMPORTANT:**
1. Market creation flow
2. User profile updates
3. Real-time price updates
4. Chart rendering
5. Filter and sort markets
6. Mobile responsive layout
## Best Practices
**DO:**
- ✅ Use Page Object Model for maintainability
- ✅ Use data-testid attributes for selectors
- ✅ Wait for API responses, not arbitrary timeouts
- ✅ Test critical user journeys end-to-end
- ✅ Run tests before merging to main
- ✅ Review artifacts when tests fail
**DON'T:**
- ❌ Use brittle selectors (CSS classes can change)
- ❌ Test implementation details
- ❌ Run tests against production
- ❌ Ignore flaky tests
- ❌ Skip artifact review on failures
- ❌ Test every edge case with E2E (use unit tests)
## Important Notes
**CRITICAL for PMX:**
- E2E tests involving real money MUST run on testnet/staging only
- Never run trading tests against production
- Set `test.skip(process.env.NODE_ENV === 'production')` for financial tests
- Use test wallets with small test funds only
## Integration with Other Commands
- Use `/plan` to identify critical journeys to test
- Use `/tdd` for unit tests (faster, more granular)
- Use `/e2e` for integration and user journey tests
- Use `/code-review` to verify test quality
## Related Agents
This command invokes the `e2e-runner` agent located at:
`.cursor/agents/e2e-runner.md`
And can reference the NestJS E2E skill at:
`.agents/skills/nestjs-best-practices/rules/test-e2e-supertest.md`
## Quick Commands
```bash
# Run all E2E tests
npm run test:e2e
# Run a specific E2E file
npx jest --config ./test/jest-e2e.json test/auth.e2e-spec.ts
# Watch mode during development
npx jest --config ./test/jest-e2e.json --watch
```
+120
View File
@@ -0,0 +1,120 @@
# Eval Command
Manage eval-driven development workflow.
## Usage
`/eval [define|check|report|list] [feature-name]`
## Define Evals
`/eval define feature-name`
Create a new eval definition:
1. Create `.cursor/evals/feature-name.md` with template:
```markdown
## EVAL: feature-name
Created: $(date)
### Capability Evals
- [ ] [Description of capability 1]
- [ ] [Description of capability 2]
### Regression Evals
- [ ] [Existing behavior 1 still works]
- [ ] [Existing behavior 2 still works]
### Success Criteria
- pass@3 > 90% for capability evals
- pass^3 = 100% for regression evals
```
2. Prompt user to fill in specific criteria
## Check Evals
`/eval check feature-name`
Run evals for a feature:
1. Read eval definition from `.cursor/evals/feature-name.md`
2. For each capability eval:
- Attempt to verify criterion
- Record PASS/FAIL
- Log attempt in `.cursor/evals/feature-name.log`
3. For each regression eval:
- Run relevant tests
- Compare against baseline
- Record PASS/FAIL
4. Report current status:
```
EVAL CHECK: feature-name
========================
Capability: X/Y passing
Regression: X/Y passing
Status: IN PROGRESS / READY
```
## Report Evals
`/eval report feature-name`
Generate comprehensive eval report:
```
EVAL REPORT: feature-name
=========================
Generated: $(date)
CAPABILITY EVALS
----------------
[eval-1]: PASS (pass@1)
[eval-2]: PASS (pass@2) - required retry
[eval-3]: FAIL - see notes
REGRESSION EVALS
----------------
[test-1]: PASS
[test-2]: PASS
[test-3]: PASS
METRICS
-------
Capability pass@1: 67%
Capability pass@3: 100%
Regression pass^3: 100%
NOTES
-----
[Any issues, edge cases, or observations]
RECOMMENDATION
--------------
[SHIP / NEEDS WORK / BLOCKED]
```
## List Evals
`/eval list`
Show all eval definitions:
```
EVAL DEFINITIONS
================
feature-auth [3/5 passing] IN PROGRESS
feature-search [5/5 passing] READY
feature-export [0/4 passing] NOT STARTED
```
## Arguments
$ARGUMENTS:
- `define <name>` - Create new eval definition
- `check <name>` - Run and check evals
- `report <name>` - Generate full report
- `list` - Show all evals
- `clean` - Remove old eval logs (keeps last 10 runs)
+70
View File
@@ -0,0 +1,70 @@
# /learn - Extract Reusable Patterns
Analyze the current session and extract any patterns worth saving as skills.
## Trigger
Run `/learn` at any point during a session when you've solved a non-trivial problem.
## What to Extract
Look for:
1. **Error Resolution Patterns**
- What error occurred?
- What was the root cause?
- What fixed it?
- Is this reusable for similar errors?
2. **Debugging Techniques**
- Non-obvious debugging steps
- Tool combinations that worked
- Diagnostic patterns
3. **Workarounds**
- Library quirks
- API limitations
- Version-specific fixes
4. **Project-Specific Patterns**
- Codebase conventions discovered
- Architecture decisions made
- Integration patterns
## Output Format
Create a skill file at `.agents/skills/learned/[pattern-name].md`:
```markdown
# [Descriptive Pattern Name]
**Extracted:** [Date]
**Context:** [Brief description of when this applies]
## Problem
[What problem this solves - be specific]
## Solution
[The pattern/technique/workaround]
## Example
[Code example if applicable]
## When to Use
[Trigger conditions - what should activate this skill]
```
## Process
1. Review the session for extractable patterns
2. Identify the most valuable/reusable insight
3. Draft the skill file
4. Ask user to confirm before saving
5. Save to `.agents/skills/learned/`
## Notes
- Don't extract trivial fixes (typos, simple syntax errors)
- Don't extract one-time issues (specific API outages, etc.)
- Focus on patterns that will save time in future sessions
- Keep skills focused - one pattern per skill
+172
View File
@@ -0,0 +1,172 @@
# Orchestrate Command
Sequential agent workflow for complex tasks.
## Usage
`/orchestrate [workflow-type] [task-description]`
## Workflow Types
### feature
Full feature implementation workflow:
```
planner -> tdd-guide -> code-reviewer -> security-reviewer
```
### bugfix
Bug investigation and fix workflow:
```
explorer -> tdd-guide -> code-reviewer
```
### refactor
Safe refactoring workflow:
```
architect -> code-reviewer -> tdd-guide
```
### security
Security-focused review:
```
security-reviewer -> code-reviewer -> architect
```
## Execution Pattern
For each agent in the workflow:
1. **Invoke agent** with context from previous agent
2. **Collect output** as structured handoff document
3. **Pass to next agent** in chain
4. **Aggregate results** into final report
## Handoff Document Format
Between agents, create handoff document:
```markdown
## HANDOFF: [previous-agent] -> [next-agent]
### Context
[Summary of what was done]
### Findings
[Key discoveries or decisions]
### Files Modified
[List of files touched]
### Open Questions
[Unresolved items for next agent]
### Recommendations
[Suggested next steps]
```
## Example: Feature Workflow
```
/orchestrate feature "Add user authentication"
```
Executes:
1. **Planner Agent**
- Analyzes requirements
- Creates implementation plan
- Identifies dependencies
- Output: `HANDOFF: planner -> tdd-guide`
2. **TDD Guide Agent**
- Reads planner handoff
- Writes tests first
- Implements to pass tests
- Output: `HANDOFF: tdd-guide -> code-reviewer`
3. **Code Reviewer Agent**
- Reviews implementation
- Checks for issues
- Suggests improvements
- Output: `HANDOFF: code-reviewer -> security-reviewer`
4. **Security Reviewer Agent**
- Security audit
- Vulnerability check
- Final approval
- Output: Final Report
## Final Report Format
```
ORCHESTRATION REPORT
====================
Workflow: feature
Task: Add user authentication
Agents: planner -> tdd-guide -> code-reviewer -> security-reviewer
SUMMARY
-------
[One paragraph summary]
AGENT OUTPUTS
-------------
Planner: [summary]
TDD Guide: [summary]
Code Reviewer: [summary]
Security Reviewer: [summary]
FILES CHANGED
-------------
[List all files modified]
TEST RESULTS
------------
[Test pass/fail summary]
SECURITY STATUS
---------------
[Security findings]
RECOMMENDATION
--------------
[SHIP / NEEDS WORK / BLOCKED]
```
## Parallel Execution
For independent checks, run agents in parallel:
```markdown
### Parallel Phase
Run simultaneously:
- code-reviewer (quality)
- security-reviewer (security)
- architect (design)
### Merge Results
Combine outputs into single report
```
## Arguments
$ARGUMENTS:
- `feature <description>` - Full feature workflow
- `bugfix <description>` - Bug fix workflow
- `refactor <description>` - Refactoring workflow
- `security <description>` - Security review workflow
- `custom <agents> <description>` - Custom agent sequence
## Custom Workflow Example
```
/orchestrate custom "architect,tdd-guide,code-reviewer" "Redesign caching layer"
```
## Tips
1. **Start with planner** for complex features
2. **Always include code-reviewer** before merge
3. **Use security-reviewer** for auth/payment/PII
4. **Keep handoffs concise** - focus on what next agent needs
5. **Run verification** between agents if needed
+112
View File
@@ -0,0 +1,112 @@
---
description: Restate requirements, assess risks, and create step-by-step implementation plan. WAIT for user CONFIRM before touching any code.
---
# Plan Command
This command invokes the **planner** agent to create a comprehensive implementation plan before writing any code.
## What This Command Does
1. **Restate Requirements** - Clarify what needs to be built
2. **Identify Risks** - Surface potential issues and blockers
3. **Create Step Plan** - Break down implementation into phases
4. **Wait for Confirmation** - MUST receive user approval before proceeding
## When to Use
Use `/plan` when:
- Starting a new feature
- Making significant architectural changes
- Working on complex refactoring
- Multiple files/components will be affected
- Requirements are unclear or ambiguous
## How It Works
The planner agent will:
1. **Analyze the request** and restate requirements in clear terms
2. **Break down into phases** with specific, actionable steps
3. **Identify dependencies** between components
4. **Assess risks** and potential blockers
5. **Estimate complexity** (High/Medium/Low)
6. **Present the plan** and WAIT for your explicit confirmation
## Example Usage
```
User: /plan I need to add real-time notifications when markets resolve
Agent (planner):
# Implementation Plan: Real-Time Market Resolution Notifications
## Requirements Restatement
- Send notifications to users when markets they're watching resolve
- Support multiple notification channels (in-app, email, webhook)
- Ensure notifications are delivered reliably
- Include market outcome and user's position result
## Implementation Phases
### Phase 1: Database Schema
- Add notifications table with columns: id, user_id, market_id, type, status, created_at
- Add user_notification_preferences table for channel preferences
- Create indexes on user_id and market_id for performance
### Phase 2: Notification Service
- Create NotificationsModule under src/modules/notifications/
- Implement notification queue using BullMQ/Redis
- Add retry logic for failed deliveries
- Create notification templates
### Phase 3: Integration Points
- Hook into market resolution logic (when status changes to "resolved")
- Query all users with positions in market
- Enqueue notifications for each user
### Phase 4: API
- Expose notification endpoints on NotificationsController
- Add NestJS WebSocket gateway for live updates
- Add notification preferences endpoints
## Dependencies
- Redis (for queue)
- Email service (SendGrid/Resend)
- NestJS WebSockets (or Redis pub/sub)
## Risks
- HIGH: Email deliverability (SPF/DKIM required)
- MEDIUM: Performance with 1000+ users per market
- MEDIUM: Notification spam if markets resolve frequently
- LOW: Real-time subscription overhead
## Estimated Complexity: MEDIUM
- Backend: 4-6 hours
- Frontend: 3-4 hours
- Testing: 2-3 hours
- Total: 9-13 hours
**WAITING FOR CONFIRMATION**: Proceed with this plan? (yes/no/modify)
```
## Important Notes
**CRITICAL**: The planner agent will **NOT** write any code until you explicitly confirm the plan with "yes" or "proceed" or similar affirmative response.
If you want changes, respond with:
- "modify: [your changes]"
- "different approach: [alternative]"
- "skip phase 2 and do phase 3 first"
## Integration with Other Commands
After planning:
- Use `/tdd` to implement with test-driven development
- Use `/build-and-fix` if build errors occur
- Use `/code-review` to review completed implementation
## Related Agents
This command invokes the `planner` agent located at:
`.cursor/agents/planner.md`
+28
View File
@@ -0,0 +1,28 @@
# Refactor Clean
Safely identify and remove dead code with test verification:
1. Run dead code analysis tools:
- knip: Find unused exports and files
- depcheck: Find unused dependencies
- ts-prune: Find unused TypeScript exports
2. Generate comprehensive report in .reports/dead-code-analysis.md
3. Categorize findings by severity:
- SAFE: Test files, unused utilities
- CAUTION: API routes, components
- DANGER: Config files, main entry points
4. Propose safe deletions only
5. Before each deletion:
- Run full test suite
- Verify tests pass
- Apply change
- Re-run tests
- Rollback if tests fail
6. Show summary of cleaned items
Never delete code without running tests first!
+80
View File
@@ -0,0 +1,80 @@
---
description: Configure your preferred package manager (npm/pnpm/yarn/bun)
disable-model-invocation: true
---
# Package Manager Setup
Configure your preferred package manager for this project or globally.
## Usage
```bash
# Detect current package manager
node scripts/setup-package-manager.js --detect
# Set global preference
node scripts/setup-package-manager.js --global pnpm
# Set project preference
node scripts/setup-package-manager.js --project bun
# List available package managers
node scripts/setup-package-manager.js --list
```
## Detection Priority
When determining which package manager to use, the following order is checked:
1. **Environment variable**: `CURSOR_PACKAGE_MANAGER`
2. **Project config**: `.cursor/package-manager.json`
3. **package.json**: `packageManager` field
4. **Lock file**: Presence of package-lock.json, yarn.lock, pnpm-lock.yaml, or bun.lockb
5. **Global config**: `~/.cursor/package-manager.json`
6. **Fallback**: First available package manager (pnpm > bun > yarn > npm)
## Configuration Files
### Global Configuration
```json
// ~/.cursor/package-manager.json
{
"packageManager": "pnpm"
}
```
### Project Configuration
```json
// .cursor/package-manager.json
{
"packageManager": "bun"
}
```
### package.json
```json
{
"packageManager": "pnpm@8.6.0"
}
```
## Environment Variable
Set `CURSOR_PACKAGE_MANAGER` to override all other detection methods:
```bash
# Windows (PowerShell)
$env:CURSOR_PACKAGE_MANAGER = "pnpm"
# macOS/Linux
export CURSOR_PACKAGE_MANAGER=pnpm
```
## Run the Detection
To see current package manager detection results, run:
```bash
node scripts/setup-package-manager.js --detect
```
+326
View File
@@ -0,0 +1,326 @@
---
description: Enforce test-driven development workflow. Scaffold interfaces, generate tests FIRST, then implement minimal code to pass. Ensure 80%+ coverage.
---
# TDD Command
This command invokes the **tdd-guide** agent to enforce test-driven development methodology.
## What This Command Does
1. **Scaffold Interfaces** - Define types/interfaces first
2. **Generate Tests First** - Write failing tests (RED)
3. **Implement Minimal Code** - Write just enough to pass (GREEN)
4. **Refactor** - Improve code while keeping tests green (REFACTOR)
5. **Verify Coverage** - Ensure 80%+ test coverage
## When to Use
Use `/tdd` when:
- Implementing new features
- Adding new functions/components
- Fixing bugs (write test that reproduces bug first)
- Refactoring existing code
- Building critical business logic
## How It Works
The tdd-guide agent will:
1. **Define interfaces** for inputs/outputs
2. **Write tests that will FAIL** (because code doesn't exist yet)
3. **Run tests** and verify they fail for the right reason
4. **Write minimal implementation** to make tests pass
5. **Run tests** and verify they pass
6. **Refactor** code while keeping tests green
7. **Check coverage** and add more tests if below 80%
## TDD Cycle
```
RED → GREEN → REFACTOR → REPEAT
RED: Write a failing test
GREEN: Write minimal code to pass
REFACTOR: Improve code, keep tests passing
REPEAT: Next feature/scenario
```
## Example Usage
```
User: /tdd I need a function to calculate market liquidity score
Agent (tdd-guide):
# TDD Session: Market Liquidity Score Calculator
## Step 1: Define Interface (SCAFFOLD)
```typescript
// lib/liquidity.ts
export interface MarketData {
totalVolume: number
bidAskSpread: number
activeTraders: number
lastTradeTime: Date
}
export function calculateLiquidityScore(market: MarketData): number {
// TODO: Implementation
throw new Error('Not implemented')
}
```
## Step 2: Write Failing Test (RED)
```typescript
// lib/liquidity.test.ts
import { calculateLiquidityScore } from './liquidity'
describe('calculateLiquidityScore', () => {
it('should return high score for liquid market', () => {
const market = {
totalVolume: 100000,
bidAskSpread: 0.01,
activeTraders: 500,
lastTradeTime: new Date()
}
const score = calculateLiquidityScore(market)
expect(score).toBeGreaterThan(80)
expect(score).toBeLessThanOrEqual(100)
})
it('should return low score for illiquid market', () => {
const market = {
totalVolume: 100,
bidAskSpread: 0.5,
activeTraders: 2,
lastTradeTime: new Date(Date.now() - 86400000) // 1 day ago
}
const score = calculateLiquidityScore(market)
expect(score).toBeLessThan(30)
expect(score).toBeGreaterThanOrEqual(0)
})
it('should handle edge case: zero volume', () => {
const market = {
totalVolume: 0,
bidAskSpread: 0,
activeTraders: 0,
lastTradeTime: new Date()
}
const score = calculateLiquidityScore(market)
expect(score).toBe(0)
})
})
```
## Step 3: Run Tests - Verify FAIL
```bash
npm test lib/liquidity.test.ts
FAIL lib/liquidity.test.ts
✕ should return high score for liquid market (2 ms)
Error: Not implemented
1 test failed, 0 passed
```
✅ Tests fail as expected. Ready to implement.
## Step 4: Implement Minimal Code (GREEN)
```typescript
// lib/liquidity.ts
export function calculateLiquidityScore(market: MarketData): number {
// Handle zero volume edge case
if (market.totalVolume === 0) {
return 0
}
// Calculate component scores (0-100 scale)
const volumeScore = Math.min(market.totalVolume / 1000, 100)
const spreadScore = Math.max(100 - (market.bidAskSpread * 1000), 0)
const traderScore = Math.min(market.activeTraders / 10, 100)
// Recent activity bonus
const hoursSinceLastTrade = (Date.now() - market.lastTradeTime.getTime()) / (1000 * 60 * 60)
const recencyScore = Math.max(100 - (hoursSinceLastTrade * 10), 0)
// Weighted average
const score = (
volumeScore * 0.4 +
spreadScore * 0.3 +
traderScore * 0.2 +
recencyScore * 0.1
)
return Math.min(Math.max(score, 0), 100) // Clamp to 0-100
}
```
## Step 5: Run Tests - Verify PASS
```bash
npm test lib/liquidity.test.ts
PASS lib/liquidity.test.ts
✓ should return high score for liquid market (3 ms)
✓ should return low score for illiquid market (2 ms)
✓ should handle edge case: zero volume (1 ms)
3 tests passed
```
✅ All tests passing!
## Step 6: Refactor (IMPROVE)
```typescript
// lib/liquidity.ts - Refactored with constants and better readability
const WEIGHTS = {
VOLUME: 0.4,
SPREAD: 0.3,
TRADERS: 0.2,
RECENCY: 0.1,
} as const
const SCALE_FACTORS = {
VOLUME: 1000,
SPREAD: 1000,
TRADERS: 10,
RECENCY_PENALTY: 10,
} as const
function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max)
}
export function calculateLiquidityScore(market: MarketData): number {
if (market.totalVolume === 0) return 0
const volumeScore = Math.min(market.totalVolume / SCALE_FACTORS.VOLUME, 100)
const spreadScore = clamp(100 - (market.bidAskSpread * SCALE_FACTORS.SPREAD), 0, 100)
const traderScore = Math.min(market.activeTraders / SCALE_FACTORS.TRADERS, 100)
const hoursSinceLastTrade = (Date.now() - market.lastTradeTime.getTime()) / (1000 * 60 * 60)
const recencyScore = clamp(100 - (hoursSinceLastTrade * SCALE_FACTORS.RECENCY_PENALTY), 0, 100)
const weightedScore =
volumeScore * WEIGHTS.VOLUME +
spreadScore * WEIGHTS.SPREAD +
traderScore * WEIGHTS.TRADERS +
recencyScore * WEIGHTS.RECENCY
return clamp(weightedScore, 0, 100)
}
```
## Step 7: Verify Tests Still Pass
```bash
npm test lib/liquidity.test.ts
PASS lib/liquidity.test.ts
✓ should return high score for liquid market (3 ms)
✓ should return low score for illiquid market (2 ms)
✓ should handle edge case: zero volume (1 ms)
3 tests passed
```
✅ Refactoring complete, tests still passing!
## Step 8: Check Coverage
```bash
npm test -- --coverage lib/liquidity.test.ts
File | % Stmts | % Branch | % Funcs | % Lines
---------------|---------|----------|---------|--------
liquidity.ts | 100 | 100 | 100 | 100
Coverage: 100% ✅ (Target: 80%)
```
✅ TDD session complete!
```
## TDD Best Practices
**DO:**
- ✅ Write the test FIRST, before any implementation
- ✅ Run tests and verify they FAIL before implementing
- ✅ Write minimal code to make tests pass
- ✅ Refactor only after tests are green
- ✅ Add edge cases and error scenarios
- ✅ Aim for 80%+ coverage (100% for critical code)
**DON'T:**
- ❌ Write implementation before tests
- ❌ Skip running tests after each change
- ❌ Write too much code at once
- ❌ Ignore failing tests
- ❌ Test implementation details (test behavior)
- ❌ Mock everything (prefer integration tests)
## Test Types to Include
**Unit Tests** (Function-level):
- Happy path scenarios
- Edge cases (empty, null, max values)
- Error conditions
- Boundary values
**Integration Tests** (Component-level):
- API endpoints
- Database operations
- External service calls
- React components with hooks
**E2E Tests** (use `/e2e` command):
- Critical user flows
- Multi-step processes
- Full stack integration
## Coverage Requirements
- **80% minimum** for all code
- **100% required** for:
- Financial calculations
- Authentication logic
- Security-critical code
- Core business logic
## Important Notes
**MANDATORY**: Tests must be written BEFORE implementation. The TDD cycle is:
1. **RED** - Write failing test
2. **GREEN** - Implement to pass
3. **REFACTOR** - Improve code
Never skip the RED phase. Never write code before tests.
## Integration with Other Commands
- Use `/plan` first to understand what to build
- Use `/tdd` to implement with tests
- Use `/build-and-fix` if build errors occur
- Use `/code-review` to review implementation
- Use `/test-coverage` to verify coverage
## Related Agents
This command invokes the `tdd-guide` agent located at:
`.cursor/agents/tdd-guide.md`
And can reference the `tdd-workflow` skill at:
`.agents/skills/tdd-workflow/`
+27
View File
@@ -0,0 +1,27 @@
# Test Coverage
Analyze test coverage and generate missing tests:
1. Run tests with coverage: npm test --coverage or pnpm test --coverage
2. Analyze coverage report (coverage/coverage-summary.json)
3. Identify files below 80% coverage threshold
4. For each under-covered file:
- Analyze untested code paths
- Generate unit tests for functions
- Generate integration tests for APIs
- Generate E2E tests for critical flows
5. Verify new tests pass
6. Show before/after coverage metrics
7. Ensure project reaches 80%+ overall coverage
Focus on:
- Happy path scenarios
- Error handling
- Edge cases (null, undefined, empty)
- Boundary conditions
+17
View File
@@ -0,0 +1,17 @@
# Update Codemaps
Analyze the codebase structure and update architecture documentation:
1. Scan all source files for imports, exports, and dependencies
2. Generate token-lean codemaps in the following format:
- codemaps/architecture.md - Overall architecture
- codemaps/backend.md - Backend structure
- codemaps/frontend.md - Frontend structure
- codemaps/data.md - Data models and schemas
3. Calculate diff percentage from previous version
4. If changes > 30%, request user approval before updating
5. Add freshness timestamp to each codemap
6. Save reports to .reports/codemap-diff.txt
Use TypeScript/Node.js for analysis. Focus on high-level structure, not implementation details.
+31
View File
@@ -0,0 +1,31 @@
# Update Documentation
Sync documentation from source-of-truth:
1. Read package.json scripts section
- Generate scripts reference table
- Include descriptions from comments
2. Read .env.example
- Extract all environment variables
- Document purpose and format
3. Generate docs/CONTRIB.md with:
- Development workflow
- Available scripts
- Environment setup
- Testing procedures
4. Generate docs/RUNBOOK.md with:
- Deployment procedures
- Monitoring and alerts
- Common issues and fixes
- Rollback procedures
5. Identify obsolete documentation:
- Find docs not modified in 90+ days
- List for manual review
6. Show diff summary
Single source of truth: package.json and .env.example
+59
View File
@@ -0,0 +1,59 @@
# Verification Command
Run comprehensive verification on current codebase state.
## Instructions
Execute verification in this exact order:
1. **Build Check**
- Run the build command for this project
- If it fails, report errors and STOP
2. **Type Check**
- Run TypeScript/type checker
- Report all errors with file:line
3. **Lint Check**
- Run linter
- Report warnings and errors
4. **Test Suite**
- Run all tests
- Report pass/fail count
- Report coverage percentage
5. **Console.log Audit**
- Search for console.log in source files
- Report locations
6. **Git Status**
- Show uncommitted changes
- Show files modified since last commit
## Output
Produce a concise verification report:
```
VERIFICATION: [PASS/FAIL]
Build: [OK/FAIL]
Types: [OK/X errors]
Lint: [OK/X issues]
Tests: [X/Y passed, Z% coverage]
Secrets: [OK/X found]
Logs: [OK/X console.logs]
Ready for PR: [YES/NO]
```
If any critical issues, list them with fix suggestions.
## Arguments
$ARGUMENTS can be:
- `quick` - Only build + types
- `full` - All checks (default)
- `pre-commit` - Checks relevant for commits
- `pre-pr` - Full checks plus security scan
+20
View File
@@ -0,0 +1,20 @@
# Development Context
Mode: Active development
Focus: Implementation, coding, building features
## Behavior
- Write code first, explain after
- Prefer working solutions over perfect solutions
- Run tests after changes
- Keep commits atomic
## Priorities
1. Get it working
2. Get it right
3. Get it clean
## Tools to favor
- Edit, Write for code changes
- Bash for running tests/builds
- Grep, Glob for finding code
+26
View File
@@ -0,0 +1,26 @@
# Research Context
Mode: Exploration, investigation, learning
Focus: Understanding before acting
## Behavior
- Read widely before concluding
- Ask clarifying questions
- Document findings as you go
- Don't write code until understanding is clear
## Research Process
1. Understand the question
2. Explore relevant code/docs
3. Form hypothesis
4. Verify with evidence
5. Summarize findings
## Tools to favor
- Read for understanding code
- Grep, Glob for finding patterns
- WebSearch, WebFetch for external docs
- Task with Explore agent for codebase questions
## Output
Findings first, recommendations second
+22
View File
@@ -0,0 +1,22 @@
# Code Review Context
Mode: PR review, code analysis
Focus: Quality, security, maintainability
## Behavior
- Read thoroughly before commenting
- Prioritize issues by severity (critical > high > medium > low)
- Suggest fixes, don't just point out problems
- Check for security vulnerabilities
## Review Checklist
- [ ] Logic errors
- [ ] Edge cases
- [ ] Error handling
- [ ] Security (injection, auth, secrets)
- [ ] Performance
- [ ] Readability
- [ ] Test coverage
## Output Format
Group findings by file, severity first
+34
View File
@@ -0,0 +1,34 @@
{
"version": 1,
"hooks": {
"sessionStart": [
{
"command": "node .cursor/scripts/hooks/session-start.js"
}
],
"sessionEnd": [
{
"command": "node .cursor/scripts/hooks/session-end.js"
},
{
"command": "node .cursor/scripts/hooks/evaluate-session.js"
}
],
"preCompact": [
{
"command": "node .cursor/scripts/hooks/pre-compact.js"
}
],
"afterFileEdit": [
{
"command": "node .cursor/scripts/hooks/suggest-compact.js"
}
],
"beforeShellExecution": [
{
"command": ".cursor/hooks/before-shell.sh",
"matcher": "npm run dev|pnpm( run)? dev|yarn dev|bun run dev|git push"
}
]
}
}
+21
View File
@@ -0,0 +1,21 @@
#!/bin/bash
# Cursor beforeShellExecution hook.
# Remind about tmux for long-running servers and review before git push.
input=$(cat)
command=$(echo "$input" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const i=JSON.parse(d);process.stdout.write(i.command||i.tool_input?.command||'')}catch{}})")
if echo "$command" | grep -Eq 'npm run dev|pnpm( run)? dev|yarn dev|bun run dev'; then
if [ -z "$TMUX" ]; then
echo '{"permission":"ask","user_message":"Dev servers should run in tmux so logs stay available. Example: tmux new-session -d -s dev \"npm run dev\"","agent_message":"Dev server is not in tmux. Ask before continuing."}'
exit 0
fi
fi
if echo "$command" | grep -Eq 'git push'; then
echo '{"permission":"ask","user_message":"Review the diff before pushing. Continue only if the changes look correct.","agent_message":"git push requires a quick review before proceeding."}'
exit 0
fi
echo '{"permission":"allow"}'
exit 0
+28
View File
@@ -0,0 +1,28 @@
#!/bin/bash
# PreCompact Hook - Save state before context compaction
#
# Runs before Claude compacts context, giving you a chance to
# preserve important state that might get lost in summarization.
#
# Configured in .cursor/hooks.json:
# preCompact -> node .cursor/scripts/hooks/pre-compact.js
# This shell version is a fallback for environments without Node.
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
SESSIONS_DIR="${ROOT}/.cursor/sessions"
COMPACTION_LOG="${SESSIONS_DIR}/compaction-log.txt"
mkdir -p "$SESSIONS_DIR"
# Log compaction event with timestamp
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Context compaction triggered" >> "$COMPACTION_LOG"
# If there's an active session file, note the compaction
ACTIVE_SESSION=$(ls -t "$SESSIONS_DIR"/*.tmp 2>/dev/null | head -1)
if [ -n "$ACTIVE_SESSION" ]; then
echo "" >> "$ACTIVE_SESSION"
echo "---" >> "$ACTIVE_SESSION"
echo "**[Compaction occurred at $(date '+%H:%M')]** - Context was summarized" >> "$ACTIVE_SESSION"
fi
echo "[PreCompact] State saved before compaction" >&2
+53
View File
@@ -0,0 +1,53 @@
#!/bin/bash
# Stop Hook (Session End) - Persist learnings when session ends
#
# Runs when Claude session ends. Creates/updates session log file
# with timestamp for continuity tracking.
#
# Configured in .cursor/hooks.json:
# sessionEnd -> node .cursor/scripts/hooks/session-end.js
# This shell version is a fallback for environments without Node.
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
SESSIONS_DIR="${ROOT}/.cursor/sessions"
TODAY=$(date '+%Y-%m-%d')
SESSION_FILE="${SESSIONS_DIR}/${TODAY}-session.tmp"
mkdir -p "$SESSIONS_DIR"
# If session file exists for today, update the end time
if [ -f "$SESSION_FILE" ]; then
# Update Last Updated timestamp
sed -i '' "s/\*\*Last Updated:\*\*.*/\*\*Last Updated:\*\* $(date '+%H:%M')/" "$SESSION_FILE" 2>/dev/null || \
sed -i "s/\*\*Last Updated:\*\*.*/\*\*Last Updated:\*\* $(date '+%H:%M')/" "$SESSION_FILE" 2>/dev/null
echo "[SessionEnd] Updated session file: $SESSION_FILE" >&2
else
# Create new session file with template
cat > "$SESSION_FILE" << EOF
# Session: $(date '+%Y-%m-%d')
**Date:** $TODAY
**Started:** $(date '+%H:%M')
**Last Updated:** $(date '+%H:%M')
---
## Current State
[Session context goes here]
### Completed
- [ ]
### In Progress
- [ ]
### Notes for Next Session
-
### Context to Load
\`\`\`
[relevant files]
\`\`\`
EOF
echo "[SessionEnd] Created session file: $SESSION_FILE" >&2
fi
+24
View File
@@ -0,0 +1,24 @@
#!/bin/bash
# SessionStart Hook - Load previous context on new session
#
# Configured in .cursor/hooks.json:
# sessionStart -> node .cursor/scripts/hooks/session-start.js
# This shell version is a fallback for environments without Node.
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
SESSIONS_DIR="${ROOT}/.cursor/sessions"
LEARNED_DIR="${ROOT}/.agents/skills/learned"
recent_sessions=$(find "$SESSIONS_DIR" -name "*.tmp" -mtime -7 2>/dev/null | wc -l | tr -d ' ')
if [ "$recent_sessions" -gt 0 ]; then
latest=$(ls -t "$SESSIONS_DIR"/*.tmp 2>/dev/null | head -1)
echo "[SessionStart] Found $recent_sessions recent session(s)" >&2
echo "[SessionStart] Latest: $latest" >&2
fi
learned_count=$(find "$LEARNED_DIR" -name "*.md" 2>/dev/null | wc -l | tr -d ' ')
if [ "$learned_count" -gt 0 ]; then
echo "[SessionStart] $learned_count learned skill(s) available in $LEARNED_DIR" >&2
fi
+43
View File
@@ -0,0 +1,43 @@
#!/bin/bash
# Strategic Compact Suggester
# Runs on PreToolUse or periodically to suggest manual compaction at logical intervals
#
# Why manual over auto-compact:
# - Auto-compact happens at arbitrary points, often mid-task
# - Strategic compacting preserves context through logical phases
# - Compact after exploration, before execution
# - Compact after completing a milestone, before starting next
#
# Configured in .cursor/hooks.json:
# afterFileEdit -> node .cursor/scripts/hooks/suggest-compact.js
# This shell version is a fallback for environments without Node.
#
# Criteria for suggesting compact:
# - Session has been running for extended period
# - Large number of tool calls made
# - Transitioning from research/exploration to implementation
# - Plan has been finalized
# Track tool call count (increment in a temp file)
COUNTER_FILE="/tmp/claude-tool-count-$$"
THRESHOLD=${COMPACT_THRESHOLD:-50}
# Initialize or increment counter
if [ -f "$COUNTER_FILE" ]; then
count=$(cat "$COUNTER_FILE")
count=$((count + 1))
echo "$count" > "$COUNTER_FILE"
else
echo "1" > "$COUNTER_FILE"
count=1
fi
# Suggest compact after threshold tool calls
if [ "$count" -eq "$THRESHOLD" ]; then
echo "[StrategicCompact] $THRESHOLD tool calls reached - consider /compact if transitioning phases" >&2
fi
# Suggest at regular intervals after threshold
if [ "$count" -gt "$THRESHOLD" ] && [ $((count % 25)) -eq 0 ]; then
echo "[StrategicCompact] $count tool calls - good checkpoint for /compact if context is stale" >&2
fi
+64
View File
@@ -0,0 +1,64 @@
---
description: When to use planner, architect, tdd-guide, code-reviewer, and other agents; parallel Task execution
alwaysApply: true
---
# Agent Orchestration
## Layout
- Agents: `.cursor/agents/`
- Skills: `.agents/skills/`
- Rules: `.cursor/rules/`
- Hooks: `.cursor/hooks.json` and `.cursor/hooks/`
## Available Agents
| Agent | Purpose | When to Use |
| -------------------- | ----------------------- | ----------------------------- |
| planner | Implementation planning | Complex features, refactoring |
| architect | System design | Architectural decisions |
| tdd-guide | Test-driven development | New features, bug fixes |
| code-reviewer | Code review | After writing code |
| security-reviewer | Security analysis | Before commits |
| build-error-resolver | Fix build errors | When build fails |
| e2e-runner | NestJS E2E testing | Critical API flows |
| refactor-cleaner | Dead code cleanup | Code maintenance |
| doc-updater | Documentation | Updating docs |
## Immediate Agent Usage
No user prompt needed:
1. Complex feature requests - Use **planner** agent
2. Code just written/modified - Use **code-reviewer** agent
3. Bug fix or new feature - Use **tdd-guide** agent
4. Architectural decision - Use **architect** agent
## Parallel Task Execution
ALWAYS use parallel Task execution for independent operations:
```markdown
# GOOD: Parallel execution
Launch 3 agents in parallel:
1. Agent 1: Security analysis of auth.ts
2. Agent 2: Performance review of cache system
3. Agent 3: Type checking of utils.ts
# BAD: Sequential when unnecessary
First agent 1, then agent 2, then agent 3
```
## Multi-Perspective Analysis
For complex problems, use split role sub-agents:
- Factual reviewer
- Senior engineer
- Security expert
- Consistency reviewer
- Redundancy checker
+76
View File
@@ -0,0 +1,76 @@
---
description: Immutability, file organization, error handling, and input validation
globs: "**/*.{ts,tsx,js,jsx}"
alwaysApply: false
---
# Coding Style
## Immutability (CRITICAL)
ALWAYS create new objects, NEVER mutate:
```javascript
// WRONG: Mutation
function updateUser(user, name) {
user.name = name // MUTATION!
return user
}
// CORRECT: Immutability
function updateUser(user, name) {
return {
...user,
name
}
}
```
## File Organization
MANY SMALL FILES > FEW LARGE FILES:
- High cohesion, low coupling
- 200-400 lines typical, 800 max
- Extract utilities from large components
- Organize by feature/domain, not by type
## Error Handling
ALWAYS handle errors comprehensively:
```typescript
try {
const result = await riskyOperation()
return result
} catch (error) {
console.error('Operation failed:', error)
throw new Error('Detailed user-friendly message')
}
```
## Input Validation
ALWAYS validate user input with class-validator DTOs (and Zod where DTOs are not used):
```typescript
import { z } from 'zod'
const schema = z.object({
email: z.string().email(),
age: z.number().int().min(0).max(150)
})
const validated = schema.parse(input)
```
## Code Quality Checklist
Before marking work complete:
- [ ] Code is readable and well-named
- [ ] Functions are small (<50 lines)
- [ ] Files are focused (<800 lines)
- [ ] No deep nesting (>4 levels)
- [ ] Proper error handling
- [ ] No console.log statements
- [ ] No hardcoded values
- [ ] No mutation (immutable patterns used)
+53
View File
@@ -0,0 +1,53 @@
---
description: All date-related data must use DateTime; never add parallel date files or helpers — extend the VO instead
alwaysApply: true
---
# DateTime Value Object
## Mandatory
ALL data related to dates or date-times in the domain and application layers MUST use `DateTime` from `src/common/value-objects/date-time/`.
- Construct from HTTP ISO via `DateTime.create(raw)`
- Reconstruct from DB via `DateTime.fromUnixMs(ms)`
- Compare with `equals()`, persist with `value` / `toJSON()` (unix **milliseconds**, UTC instant)
- Render with `format()` / `toString()` using `DEFAULT_TIMEZONE` (default `GMT+7`) — timezone is for display and naive ISO interpretation only
There is one date abstraction in this codebase: `DateTime`. Use it for every timestamp, occurrence, schedule instant, created/updated field, and any other date-related value in domain/application code.
## Forbidden
Do NOT:
- Store or pass date-times as plain `string` / `number` / built-in `Date` in domain models, services, or repositories (beyond the DTO/HTTP or DB number boundary)
- Create **any** new file or function for date parsing, validation, formatting, timezone conversion, or comparison
- Add pipes, decorators, utils, helpers, or modules that bypass the VO
- Use date libraries (`date-fns`, `luxon`, `moment`, `dayjs`, Temporal polyfills, etc.)
```typescript
// BAD — new helper / parallel logic
function parseDate(raw: string): number { /* ... */ }
function formatDate(ms: number): string { /* ... */ }
user.occurredAt = Date.parse(dto.occurredAt)
// GOOD — DateTime only
const occurredAt = DateTime.create(dto.occurredAt)
user.occurredAt = occurredAt // DateTime in domain
await repo.save({ occurredAt: occurredAt.value }) // unix ms only at persistence edge
```
## When the VO is not enough
If a requirement cannot be met with the current VO (e.g. new input formats, formatting options, comparisons, timezone forms):
1. **Update** `src/common/value-objects/date-time/` (implementation + colocated tests) to match the requirement
2. Do **not** create a new file, function, type, helper, or module for dates
## Boundaries
- HTTP DTOs may accept ISO `string`; map to `DateTime.create()` at the service boundary
- Database columns may store unix milliseconds (`bigint` / integer); map to/from `DateTime` in the repository
- Naive ISO strings (no `Z` / offset) are interpreted in `DEFAULT_TIMEZONE`
- `InvalidDateTimeError` is the only date validation error; do not echo raw input in messages
- Canonical storage is always UTC; never persist the display timezone as the instant
+50
View File
@@ -0,0 +1,50 @@
---
description: Commit message format, PR workflow, and feature implementation steps
alwaysApply: true
---
# Git Workflow
## Commit Message Format
```
<type>: <description>
<optional body>
```
Types: feat, fix, refactor, docs, test, chore, perf, ci
Keep commit messages conventional. Do not add AI attribution trailers.
## Pull Request Workflow
When creating PRs:
1. Analyze full commit history (not just latest commit)
2. Use `git diff [base-branch]...HEAD` to see all changes
3. Draft comprehensive PR summary
4. Include test plan with TODOs
5. Push with `-u` flag if new branch
## Feature Implementation Workflow
1. **Plan First**
- Use **planner** agent to create implementation plan
- Identify dependencies and risks
- Break down into phases
2. **TDD Approach**
- Use **tdd-guide** agent
- Write tests first (RED)
- Implement to pass tests (GREEN)
- Refactor (IMPROVE)
- Verify 80%+ coverage
3. **Code Review**
- Use **code-reviewer** agent immediately after writing code
- Address CRITICAL and HIGH issues
- Fix MEDIUM issues when possible
4. **Commit & Push**
- Detailed commit messages
- Follow conventional commits format
+50
View File
@@ -0,0 +1,50 @@
---
description: Cursor hooks system, auto-accept permissions, and TodoWrite practices
alwaysApply: true
---
# Hooks System
Project hooks live in `.cursor/hooks.json` (schema version 1) and scripts under `.cursor/hooks/` and `.cursor/scripts/hooks/`.
## Hook Events
- **sessionStart / sessionEnd**: Load and persist session context
- **preCompact**: Save state before context compaction
- **afterFileEdit**: Suggest strategic compact after edits
- **beforeShellExecution**: Gate long-running servers and `git push`
## Current Project Hooks
### sessionStart
- Load previous session files from `.cursor/sessions/`
- Detect package manager
- Report learned skills in `.agents/skills/learned/`
### sessionEnd
- Persist session state
- Evaluate the session for extractable patterns
### preCompact
- Log compaction and append a note to the active session file
### afterFileEdit
- Suggest manual compaction after many edits
### beforeShellExecution
- Ask before running dev servers outside tmux
- Ask before `git push`
## Auto-Accept Permissions
Use with caution:
- Enable for trusted, well-defined plans
- Disable for exploratory work
- Never skip permissions for git push, installs, or production commands
## TodoWrite Best Practices
Use TodoWrite to:
- Track progress on multi-step tasks
- Verify understanding of instructions
- Show granular implementation steps
+28
View File
@@ -0,0 +1,28 @@
---
description: NestJS modular TDD structure with Drizzle and PostgreSQL
globs: "**/*.{ts,js}"
alwaysApply: false
---
# NestJS Project Conventions
Stack: NestJS + PostgreSQL + Drizzle ORM. Follow `.agents/skills/nestjs-best-practices` and `.agents/skills/project-guidelines-example`.
## Modules
- One feature folder under `src/modules/`
- Each feature has `*.module.ts`, `*.controller.ts`, `*.service.ts`, `dto/`
- Share cross-cutting code via `src/common/` (filters, guards, pipes, interceptors)
## Tests
- Unit tests colocated as `*.spec.ts`
- E2E tests in `test/*.e2e-spec.ts` with Supertest
- Write tests first (RED → GREEN → REFACTOR)
- Mock Drizzle and external services, not Nest internals
## Database
- Schema and SQL migrations in `drizzle/`
- Config in `drizzle.config.ts`
- Never mutate production schema without a migration
+58
View File
@@ -0,0 +1,58 @@
---
description: NestJS API response format, feature modules, and Drizzle repository pattern
globs: "**/*.ts"
alwaysApply: false
---
# Common Patterns
## API Response Format
```typescript
interface ApiResponse<T> {
success: boolean
data?: T
error?: string
meta?: {
total: number
page: number
limit: number
}
}
```
## Feature Module
```typescript
@Module({
imports: [],
controllers: [UsersController],
providers: [UsersService, UsersRepository],
exports: [UsersService],
})
export class UsersModule {}
```
## Repository Pattern (Drizzle)
```typescript
interface Repository<T> {
findAll(filters?: Filters): Promise<T[]>
findById(id: string): Promise<T | null>
create(data: CreateDto): Promise<T>
update(id: string, data: UpdateDto): Promise<T>
delete(id: string): Promise<void>
}
```
## Skeleton Projects
When implementing new functionality:
1. Search for battle-tested NestJS module patterns
2. Use parallel agents to evaluate options:
- Security assessment
- Extensibility analysis
- Relevance scoring
- Implementation planning
3. Clone best match as foundation
4. Iterate within proven structure
+52
View File
@@ -0,0 +1,52 @@
---
description: Model selection, context window management, and build troubleshooting
alwaysApply: true
---
# Performance Optimization
## Model Selection Strategy
**Haiku 4.5** (90% of Sonnet capability, 3x cost savings):
- Lightweight agents with frequent invocation
- Pair programming and code generation
- Worker agents in multi-agent systems
**Sonnet 4.5** (Best coding model):
- Main development work
- Orchestrating multi-agent workflows
- Complex coding tasks
**Opus 4.5** (Deepest reasoning):
- Complex architectural decisions
- Maximum reasoning requirements
- Research and analysis tasks
## Context Window Management
Avoid last 20% of context window for:
- Large-scale refactoring
- Feature implementation spanning multiple files
- Debugging complex interactions
Lower context sensitivity tasks:
- Single-file edits
- Independent utility creation
- Documentation updates
- Simple bug fixes
## Ultrathink + Plan Mode
For complex tasks requiring deep reasoning:
1. Use `ultrathink` for enhanced thinking
2. Enable **Plan Mode** for structured approach
3. "Rev the engine" with multiple critique rounds
4. Use split role sub-agents for diverse analysis
## Build Troubleshooting
If build fails:
1. Use **build-error-resolver** agent
2. Analyze error messages
3. Fix incrementally
4. Verify after each fix
+47
View File
@@ -0,0 +1,47 @@
---
description: All phone-number data must use the PhoneNumber value object; extend the VO instead of adding alternate helpers
alwaysApply: true
---
# Phone Number Value Object
## Mandatory
ALL phone-number data in the domain and application layers MUST use `PhoneNumber` from `src/common/value-objects/phone-number/`.
- Construct only via `PhoneNumber.create(raw)`
- Compare with `equals()`, serialize with `value` / `toString()` / `toJSON()`
- Persist and transmit the canonical E.164 from `phone.value` (or `toString()` / `toJSON()`)
## Forbidden
Do NOT:
- Store or pass phone numbers as plain `string` / `number` in domain models, services, or repositories (beyond the DTO/HTTP or DB string boundary)
- Add phone validators, parsers, formatters, regex helpers, pipes, or decorators that bypass the VO
- Create new files or functions for phone-number validation or normalization
- Use `libphonenumber-js` (or similar) outside the PhoneNumber VO
```typescript
// BAD
function normalizePhone(raw: string): string { /* ... */ }
user.phone = '+6281234567890'
// GOOD
const phone = PhoneNumber.create(dto.phone)
user.phone = phone // PhoneNumber type in domain
await repo.save({ phoneNumber: phone.value }) // E.164 string only at persistence edge
```
## When the VO is not enough
If a requirement cannot be met with the current VO (e.g. national formats, default region, formatting for display):
1. **Update** `src/common/value-objects/phone-number/` (implementation + colocated tests)
2. Do **not** invent a parallel phone helper, type, or module
## Boundaries
- HTTP DTOs may accept `string`; map to `PhoneNumber.create()` at the service boundary
- Database columns may store E.164 `text`/`varchar`; map to/from `PhoneNumber` in the repository
- `InvalidPhoneNumberError` is the only phone validation error; do not echo raw input in messages
+41
View File
@@ -0,0 +1,41 @@
---
description: Mandatory security checks, secret management, and security response protocol
alwaysApply: true
---
# Security Guidelines
## Mandatory Security Checks
Before ANY commit:
- [ ] No hardcoded secrets (API keys, passwords, tokens)
- [ ] All user inputs validated
- [ ] SQL injection prevention (parameterized queries)
- [ ] XSS prevention (sanitized HTML)
- [ ] CSRF protection enabled
- [ ] Authentication/authorization verified
- [ ] Rate limiting on all endpoints
- [ ] Error messages don't leak sensitive data
## Secret Management
```typescript
// NEVER: Hardcoded secrets
const apiKey = "sk-proj-xxxxx"
// ALWAYS: Environment variables
const apiKey = process.env.OPENAI_API_KEY
if (!apiKey) {
throw new Error('OPENAI_API_KEY not configured')
}
```
## Security Response Protocol
If security issue found:
1. STOP immediately
2. Use **security-reviewer** agent
3. Fix CRITICAL issues before continuing
4. Rotate any exposed secrets
5. Review entire codebase for similar issues
+36
View File
@@ -0,0 +1,36 @@
---
description: TDD workflow, 80% coverage minimum, and NestJS unit/e2e requirements
alwaysApply: true
---
# Testing Requirements
## Minimum Test Coverage: 80%
Test Types (ALL required):
1. **Unit Tests** - Services, guards, pipes, utilities (`*.spec.ts`)
2. **Integration Tests** - NestJS testing module + Drizzle
3. **E2E Tests** - HTTP flows with Supertest (`test/*.e2e-spec.ts`)
## Test-Driven Development
MANDATORY workflow:
1. Write test first (RED)
2. Run test - it should FAIL
3. Write minimal implementation (GREEN)
4. Run test - it should PASS
5. Refactor (IMPROVE)
6. Verify coverage (80%+)
## Troubleshooting Test Failures
1. Use **tdd-guide** agent
2. Check test isolation
3. Verify mocks are correct
4. Fix implementation, not tests (unless tests are wrong)
## Agent Support
- **tdd-guide** - Use PROACTIVELY for new features, enforces write-tests-first
- **e2e-runner** - NestJS E2E (Supertest) specialist
- Skill: `.agents/skills/tdd-workflow/`

Some files were not shown because too many files have changed in this diff Show More