Agent skill
code-patterns
Enforce consistent implementation patterns across the codebase. Use when reviewing code, fixing inconsistencies, or implementing new features to ensure they follow established patterns.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/testing/code-patterns
SKILL.md
Code Patterns & Standards
This skill helps maintain consistency across the codebase by enforcing standardized patterns and identifying code that doesn't follow best practices.
When to Use This Skill
Invoke this skill when:
- Implementing new features or endpoints
- Reviewing or refactoring existing code
- Encountering inconsistent patterns
- User asks to "standardize" or "make consistent"
- User asks about "best practices" or "how we do X here"
Critical Patterns (MUST Follow)
1. Error Response Format
STANDARD FORMAT - Use this everywhere:
// Error responses
{
success: false,
error: string, // User-facing message
message?: string, // Technical details (optional, for debugging)
statusCode?: number // Optional
}
// Success responses
{
success: true,
data: T, // The actual response data
message?: string // Optional success message
}
❌ INCONSISTENT PATTERNS TO AVOID:
// Don't use these varying formats:
{ error: 'message' } // Missing success field
{ error: 'msg', message: 'other' } // Confusing dual messages
{ message: 'Error al...', error: 'Unknown' } // Mixed languages
res.json(data) // No wrapper at all
IMPLEMENTATION:
// In controllers
try {
const result = await someService();
return res.status(200).json({
success: true,
data: result
});
} catch (error) {
console.error('Error in operation:', error);
return res.status(500).json({
success: false,
error: 'User-friendly error message',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
2. Authentication Pattern
STANDARD - Use AuthRequest type:
import { Request, Response } from 'express';
// Define AuthRequest type if not already defined
interface AuthRequest extends Request {
user?: {
userId: string;
email: string;
role: string;
};
}
// In controllers
export const myController = async (req: AuthRequest, res: Response) => {
const userId = req.user?.userId; // ✅ Type-safe access
if (!userId) {
return res.status(401).json({
success: false,
error: 'Authentication required'
});
}
// ... rest of controller
};
❌ AVOID:
// Don't use type casting
const userId = (req as any).user?.userId; // ❌ Loses type safety
MIDDLEWARE IMPORTS:
// Use the new modular auth system
import { authenticate } from '../auth/middleware/authenticate';
import { requireAdmin } from '../auth/middleware/authorize';
// ❌ Don't use legacy imports
import { authenticate, requireAdmin } from '../middleware/auth';
3. Input Validation
STANDARD - Manual validation until Zod is implemented:
// At the start of controller functions
export const createResource = async (req: AuthRequest, res: Response) => {
const { field1, field2, field3 } = req.body;
// Validate required fields
if (!field1 || !field2) {
return res.status(400).json({
success: false,
error: 'Missing required fields: field1, field2'
});
}
// Validate enum values
if (!['option1', 'option2'].includes(field1)) {
return res.status(400).json({
success: false,
error: 'Invalid field1. Must be option1 or option2'
});
}
// Validate types/ranges
if (typeof field3 !== 'number' || field3 < 1 || field3 > 100) {
return res.status(400).json({
success: false,
error: 'field3 must be a number between 1 and 100'
});
}
// Continue with logic...
};
FUTURE - When adding Zod:
import { z } from 'zod';
const createResourceSchema = z.object({
field1: z.enum(['option1', 'option2']),
field2: z.string().min(1),
field3: z.number().min(1).max(100)
});
// Use in middleware or at controller start
const validation = createResourceSchema.safeParse(req.body);
if (!validation.success) {
return res.status(400).json({
success: false,
error: 'Validation failed',
message: validation.error.message
});
}
4. TypeScript Type Safety
STANDARD - Avoid 'any', use proper types:
// ❌ AVOID
const params: any[] = [];
const data: any = result.rows[0];
visualData?: { type: string; data: any };
// ✅ USE PROPER TYPES
const params: (string | number | boolean)[] = [];
interface SessionRow {
id: string;
name: string;
description: string;
status: string;
created_at: Date;
}
const data: SessionRow = result.rows[0];
interface VisualData {
type: 'graph' | 'geometry' | 'table' | 'diagram';
data: GraphData | GeometryData | TableData | DiagramData;
}
FOR DATABASE QUERIES:
// Define interfaces for query results
interface QueryResult {
rows: SessionRow[];
rowCount: number;
}
const result: QueryResult = await pool.query<SessionRow>(query, params);
5. Async/Await Pattern
STANDARD - Always use async/await:
// ✅ CORRECT
export const myController = async (req: AuthRequest, res: Response) => {
try {
const result = await service.doSomething();
return res.json({ success: true, data: result });
} catch (error) {
return res.status(500).json({
success: false,
error: 'Operation failed'
});
}
};
// ❌ AVOID .then() chains
service.doSomething()
.then(result => res.json(result))
.catch(error => res.status(500).json({ error }));
FOR PARALLEL OPERATIONS:
// ✅ Use Promise.all for parallel queries
const [users, attempts, sessions] = await Promise.all([
pool.query('SELECT COUNT(*) FROM users'),
pool.query('SELECT COUNT(*) FROM attempts'),
pool.query('SELECT COUNT(*) FROM sessions')
]);
Important Patterns (Highly Recommended)
6. Logging Standards
CURRENT MIXED STATE:
- Some files use emoji-rich logging:
console.log('🔐 Auth successful') - Some use simple logging:
console.error('Error:', error) - Some have minimal logging
RECOMMENDATION - Use structured logging:
// For important operations
console.log('[Controller:createResource] Starting operation', {
userId,
resourceType,
timestamp: new Date().toISOString()
});
// For errors - always include context
console.error('[Controller:createResource] Operation failed', {
error: error instanceof Error ? error.message : 'Unknown',
userId,
stack: error instanceof Error ? error.stack : undefined
});
// For debugging (can be removed in production)
console.debug('[Service:processData] Processing', { dataSize, filters });
AVOID:
- Excessive emoji logging in production code (fine for scripts/seeds)
- Logging sensitive data (passwords, tokens)
- Empty catch blocks without logging
7. Controller Structure
STANDARD PATTERN:
import { Response } from 'express';
import { AuthRequest } from '../types'; // Or define locally
import { serviceFunction } from '../services/myService';
/**
* Controller description
* @route POST /api/resource
* @access Private
*/
export const createResource = async (
req: AuthRequest,
res: Response
): Promise<void> => {
try {
// 1. Extract and validate authentication
const userId = req.user?.userId;
if (!userId) {
res.status(401).json({
success: false,
error: 'Authentication required'
});
return;
}
// 2. Extract request data
const { field1, field2 } = req.body;
const { queryParam } = req.query;
// 3. Validate inputs
if (!field1 || !field2) {
res.status(400).json({
success: false,
error: 'Missing required fields: field1, field2'
});
return;
}
// 4. Call service layer (business logic)
const result = await serviceFunction({
field1,
field2,
userId
});
// 5. Return success response
res.status(200).json({
success: true,
data: result
});
} catch (error) {
console.error('[createResource] Error:', error);
res.status(500).json({
success: false,
error: 'Failed to create resource',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
};
KEY PRINCIPLES:
- Controllers handle HTTP concerns (request/response)
- Services handle business logic
- Always use try-catch
- Log errors with context
- Return consistent response format
- Validate inputs early
- Check authentication first
8. Service Layer Pattern
WHEN TO CREATE A SERVICE:
- Business logic is complex
- Logic is reused across multiple controllers
- External API calls
- Complex database operations
- Data transformations
STRUCTURE:
// services/myService.ts
import { pool } from '../config/database';
interface CreateResourceInput {
field1: string;
field2: string;
userId: string;
}
interface ResourceResult {
id: string;
field1: string;
field2: string;
createdAt: Date;
}
/**
* Creates a new resource
*/
export async function createResource(
input: CreateResourceInput
): Promise<ResourceResult> {
const { field1, field2, userId } = input;
// Business logic here
const query = `
INSERT INTO resources (field1, field2, user_id)
VALUES ($1, $2, $3)
RETURNING *
`;
const result = await pool.query<ResourceResult>(
query,
[field1, field2, userId]
);
return result.rows[0];
}
/**
* Helper function for data transformation
*/
function transformData(raw: any): ResourceResult {
return {
id: raw.id,
field1: raw.field1,
field2: raw.field2,
createdAt: new Date(raw.created_at)
};
}
9. Database Query Building
FOR DYNAMIC QUERIES:
// ✅ RECOMMENDED PATTERN
let query = 'SELECT * FROM resources WHERE 1=1';
const params: (string | number)[] = [];
let paramCount = 1;
if (userId) {
query += ` AND user_id = $${paramCount}`;
params.push(userId);
paramCount++;
}
if (status) {
query += ` AND status = $${paramCount}`;
params.push(status);
paramCount++;
}
if (search) {
query += ` AND (name ILIKE $${paramCount} OR description ILIKE $${paramCount})`;
params.push(`%${search}%`);
paramCount++;
}
query += ' ORDER BY created_at DESC';
const result = await pool.query(query, params);
ALWAYS:
- Use parameterized queries (prevent SQL injection)
- Type your params array
- Increment paramCount properly
- Add ORDER BY for predictable results
10. Route Registration Pattern
IN index.ts:
// 1. Import at top with other routes
import resourceRoutes from './routes/resourceRoutes';
// 2. Register with other app.use calls
app.use('/api/resources', resourceRoutes);
// 3. Keep alphabetical order for easy scanning
app.use('/api/admin', adminRoutes);
app.use('/api/ai', aiRoutes);
app.use('/api/analytics', analyticsRoutes);
app.use('/api/auth', authRoutes);
app.use('/api/resources', resourceRoutes); // New route
app.use('/api/sessions', sessionRoutes);
Frontend Patterns
11. API Calls
USE THE CENTRALIZED API CLIENT:
// ✅ Use lib/api-client.ts
import { apiClient } from '@/lib/api-client';
const fetchData = async () => {
try {
const response = await apiClient.get('/endpoint');
if (response.success) {
setData(response.data);
}
} catch (error) {
handleError(error);
}
};
BENEFITS:
- Automatic token refresh
- Consistent error handling
- Base URL management
- Request/response interceptors
12. Error Handling (Frontend)
STANDARD PATTERN:
import { toast } from 'react-hot-toast';
const handleOperation = async () => {
try {
const response = await apiClient.post('/endpoint', data);
if (response.success) {
toast.success('Operation successful');
// Update state...
} else {
toast.error(response.error || 'Operation failed');
}
} catch (error) {
console.error('Operation error:', error);
const message = error instanceof Error
? error.message
: 'An unexpected error occurred';
toast.error(message);
} finally {
setLoading(false);
}
};
13. Presentational Components + Hooks Pattern (Frontend)
STANDARD - Separate logic from presentation:
This pattern ensures components remain maintainable by extracting business logic into custom hooks while keeping components focused on rendering.
When to Apply This Pattern
MUST extract hooks when:
- Component has 3+
useStatecalls - Component has complex
useEffectlogic (API calls, subscriptions, timers) - Component handles form state with validation
- Same logic could be reused across components
- Component exceeds 80 lines of non-JSX code
Can keep inline when:
- Simple presentational component with 1-2 state variables
- State is purely UI-related (hover, toggle, modal open)
- No API calls or side effects
File Structure Pattern
components/
FeatureName/
FeatureName.tsx # Presentational component
useFeatureName.ts # Business logic hook
FeatureName.types.ts # Shared types (optional)
index.ts # Barrel export (optional)
Or for simpler cases:
components/
FeatureName.tsx # Presentational component
hooks/
useFeatureName.ts # Business logic hook
Example: Complex Component Refactored
BEFORE - Mixed concerns (BAD):
// ❌ Component with mixed logic and presentation
export default function LiveSession({ sessionId }: Props) {
const [session, setSession] = useState<LiveSession | null>(null);
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
const [selectedAnswer, setSelectedAnswer] = useState<number | null>(null);
const [myAnswers, setMyAnswers] = useState<(number | null)[]>([]);
const [isLoading, setIsLoading] = useState(true);
const refreshSession = async () => {
// 20+ lines of API logic...
};
useEffect(() => {
refreshSession();
const interval = setInterval(refreshSession, 2000);
return () => clearInterval(interval);
}, [sessionId]);
// 100+ lines of JSX...
}
AFTER - Separated concerns (GOOD):
// ✅ hooks/useLiveSession.ts - All business logic
interface UseLiveSessionReturn {
session: LiveSession | null;
currentQuestionIndex: number;
selectedAnswer: number | null;
isLoading: boolean;
handleAnswerSelect: (index: number) => Promise<void>;
handleNext: () => void;
handlePrevious: () => void;
}
export function useLiveSession(sessionId: string): UseLiveSessionReturn {
const [session, setSession] = useState<LiveSession | null>(null);
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
const [selectedAnswer, setSelectedAnswer] = useState<number | null>(null);
const [isLoading, setIsLoading] = useState(true);
// All effects, API calls, and handlers here...
return {
session,
currentQuestionIndex,
selectedAnswer,
isLoading,
handleAnswerSelect,
handleNext,
handlePrevious,
};
}
// ✅ components/LiveSession.tsx - Pure presentation
export default function LiveSession({ sessionId, onExit }: LiveSessionProps) {
const {
session,
currentQuestionIndex,
selectedAnswer,
isLoading,
handleAnswerSelect,
handleNext,
handlePrevious,
} = useLiveSession(sessionId);
if (isLoading) return <LoadingSpinner />;
if (!session) return <ErrorState />;
return (
<div className="...">
{/* Pure JSX, no logic */}
</div>
);
}
Hook Design Guidelines
1. Hook Interface Design:
// ✅ GOOD - Clear, typed return object
interface UseQuizReturn {
// State
questions: Question[];
currentIndex: number;
isSubmitted: boolean;
// Actions
selectAnswer: (index: number) => void;
submitQuiz: () => Promise<void>;
resetQuiz: () => void;
// Computed values
score: number;
progress: number;
}
export function useQuiz(config: QuizConfig): UseQuizReturn { ... }
2. Separate Concerns Within Hooks:
// ✅ GOOD - Compose smaller hooks
export function useQuiz(config: QuizConfig) {
const state = useQuizState(config); // State management
const progress = useQuizProgress(state); // Progress tracking
const navigation = useQuizNavigation(state.questions.length);
return {
...state,
...progress,
...navigation,
};
}
3. Handle Loading and Error States:
// ✅ GOOD - Include all UI states
export function useDataFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
// ... fetch logic
return { data, isLoading, error, refetch };
}
Naming Conventions
| Type | Convention | Example |
|---|---|---|
| Hook file | use{Feature}.ts |
useQuizState.ts |
| Hook function | use{Feature} |
useQuizState() |
| Component file | {Feature}.tsx |
QuizCard.tsx |
| Props interface | {Component}Props |
QuizCardProps |
| Hook return type | Use{Feature}Return |
UseQuizStateReturn |
Existing Good Examples in This Codebase
Reference these files for the pattern:
hooks/useQuizState.ts- State management hookhooks/useQuizProgress.ts- Progress tracking hookhooks/useQuizNavigation.ts- Navigation hookcomponents/quiz/ZenQuiz.tsx- Component using multiple hookscomponents/ui/Card.tsx- Simple presentational component
ESLint Will Warn You
The following warnings suggest extracting a hook:
max-lines-per-functionexceeded (component too long)complexityexceeded (too many branches)max-statementsexceeded (too much logic)
14. Refactor When Touched Policy
When modifying existing components that don't follow the presentational + hooks pattern:
Substantial edits (MUST refactor):
- Adding new features that require understanding the component
- Fixing bugs that require understanding the component logic
- Any edit touching >10 lines of code
Minor edits (NO refactor required):
- Typo fixes
- Style/CSS changes
- Single-line fixes
Migration Priorities
When refactoring existing code, address in this order:
High Priority (Fix First):
- Error Response Format - Critical for frontend reliability
- Success Response Format - Ensure all endpoints use
{ success, data } - Authentication Pattern - Migrate to AuthRequest type
- Input Validation - Add missing validations
Medium Priority (Fix When Touching Code):
- TypeScript 'any' - Replace with proper types
- Logging - Add structured logging to new/modified code
- Auth Imports - Use new modular auth imports
- Presentational + Hooks - Extract hooks from complex components (see Refactor When Touched Policy)
Low Priority (Nice to Have):
- Form Validation - Consider React Hook Form + Zod for complex forms
- Query Builder - Consider library for complex dynamic queries
Code Review Checklist
When reviewing or implementing code, check:
- Error responses use
{ success: false, error: string }format - Success responses use
{ success: true, data: T }format - Controllers use
AuthRequesttype - Auth imports from new modular system
- All required fields are validated
- No use of
anytype (use proper types) - Uses async/await (not .then chains)
- Try-catch blocks around all async operations
- Errors are logged with context
- Parameterized queries (no SQL injection risk)
- Service layer for complex business logic
- Consistent with existing patterns in the codebase
Frontend Components:
- Complex components have logic extracted to hooks
- Hooks return typed interfaces
- Component focused on rendering (no inline API calls)
- No ESLint warnings for complexity/length
Examples of Pattern Violations
Violation: Inconsistent Error Format
// ❌ WRONG
res.status(500).json({ error: 'Failed' });
// ✅ CORRECT
res.status(500).json({
success: false,
error: 'Failed to process request'
});
Violation: Type Casting
// ❌ WRONG
const userId = (req as any).user?.userId;
// ✅ CORRECT
interface AuthRequest extends Request {
user?: { userId: string };
}
const userId = req.user?.userId;
Violation: Missing Validation
// ❌ WRONG
export const create = async (req: Request, res: Response) => {
const { name } = req.body;
await createResource(name); // No validation!
res.json({ success: true });
};
// ✅ CORRECT
export const create = async (req: AuthRequest, res: Response) => {
const { name } = req.body;
if (!name || typeof name !== 'string') {
return res.status(400).json({
success: false,
error: 'Name is required and must be a string'
});
}
const result = await createResource(name);
res.json({ success: true, data: result });
};
Quick Reference
New Controller Template:
import { Response } from 'express';
import { AuthRequest } from '../types';
export const myController = async (req: AuthRequest, res: Response) => {
try {
const userId = req.user?.userId;
if (!userId) {
return res.status(401).json({ success: false, error: 'Auth required' });
}
const { field } = req.body;
if (!field) {
return res.status(400).json({ success: false, error: 'Missing field' });
}
const result = await service(field, userId);
return res.status(200).json({ success: true, data: result });
} catch (error) {
console.error('[myController] Error:', error);
return res.status(500).json({
success: false,
error: 'Operation failed',
message: error instanceof Error ? error.message : 'Unknown'
});
}
};
Use this skill proactively to catch inconsistencies and maintain code quality!
Didn't find tool you were looking for?